Skip to content

Commit 4e24cd1

Browse files
authored
Merge pull request #8 from peek-travel/feature/v2
feat: support v2 registry api calls
2 parents 6ef9d9c + 5983967 commit 4e24cd1

6 files changed

Lines changed: 192 additions & 29 deletions

File tree

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Typecheck
25+
run: npm run typecheck
26+
27+
- name: Lint
28+
run: npm run lint
29+
30+
- name: Test (with coverage gate)
31+
run: npm run test:coverage

docs/internal/ARCHITECTURE.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,15 @@ call typed methods like `peek.getProductService().getAllProducts()`.
4949
guide resolution).
5050
- `BookingService` receives the product service (for add-on → parent-item
5151
resolution).
52-
- Optional config: `baseUrl`, `tokenTtlSeconds` (3600), `tokenRefreshLeewaySeconds`
52+
- Optional config: `mode` (`"v2"` — see below), `baseUrl`, `tokenTtlSeconds` (3600), `tokenRefreshLeewaySeconds`
5353
(60), `retryDelaysMs` (`[1000, 2000, 4000]`), `logger` (no-op default),
5454
`fetch` (global default), `itemOptionsPageSize` (50).
55+
- **v2 mode** (`mode: "v2"`): routes requests through the app-registry
56+
installations API. The endpoint URL becomes
57+
`baseUrl/appId/peek_backoffice_api-v1/endpointName` and the default `baseUrl`
58+
switches to `https://app-registry.peeklabs.com/installations-api`.
59+
A custom `baseUrl` still overrides the default in v2 mode. All other
60+
behaviour (JWT auth, headers, retries, resource services) is unchanged.
5561

5662
### 2. `TokenManager` — auth
5763
`src/internal/token-manager.ts`
@@ -65,8 +71,11 @@ call typed methods like `peek.getProductService().getAllProducts()`.
6571

6672
The only place that touches the network. Responsibilities:
6773

68-
- Builds the endpoint URL as `${baseUrl}/${appId}/${endpointName}`. Today every
69-
operation routes through the single `sales` endpoint (`gateway-endpoints.ts`).
74+
- Builds the endpoint URL as `${baseUrl}/${appId}/${endpointName}`, or
75+
`${baseUrl}/${appId}/${endpointPathPrefix}/${endpointName}` when an
76+
`endpointPathPrefix` is set (v2 mode inserts `peek_backoffice_api-v1`). Today
77+
every operation routes through the single `sales` endpoint
78+
(`gateway-endpoints.ts`).
7079
- Sets headers: `X-Peek-Auth: Bearer <jwt>`, `pk-api-key: <gatewayKey>`,
7180
`Content-Type: application/json`.
7281
- Collapses query whitespace (`\s+` → single space) before sending.

src/internal/gateway-endpoints.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
* so the value lives in exactly one place.
55
*/
66
export const SALES_ENDPOINT = "sales";
7+
8+
/** Fixed path segment inserted between `appId` and the endpoint name in v2 mode. */
9+
export const V2_EXTENDABLE_SLUG = "peek_backoffice_api-v1";

src/internal/graphql-client.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export interface GraphQLClientOptions {
2424
baseUrl: string;
2525
/** Peek app ID, used in the endpoint path. */
2626
appId: string;
27-
/** API gateway key sent as the `pk-api-key` header. */
28-
gatewayKey: string;
27+
/** API gateway key sent as the `pk-api-key` header. Omitted from headers when absent (v2 mode). */
28+
gatewayKey?: string;
2929
/** Supplies a valid bearer token for each request. */
3030
getToken: () => string;
3131
/** Backoff delays (ms) applied on successive HTTP 429 responses. */
@@ -34,6 +34,11 @@ export interface GraphQLClientOptions {
3434
logger: Logger;
3535
/** `fetch` implementation to use. */
3636
fetchFn: typeof fetch;
37+
/**
38+
* Optional fixed path segment inserted between `appId` and the endpoint name.
39+
* Used in v2 mode: `baseUrl/appId/endpointPathPrefix/endpointName`.
40+
*/
41+
endpointPathPrefix?: string;
3742
}
3843

3944
const sleep = (ms: number): Promise<void> =>
@@ -106,14 +111,19 @@ export class GraphQLClient {
106111
}
107112

108113
private endpoint(endpointName: string): string {
109-
return `${this.options.baseUrl}/${this.options.appId}/${endpointName}`;
114+
const { baseUrl, appId, endpointPathPrefix } = this.options;
115+
const prefix = endpointPathPrefix ? `${endpointPathPrefix}/` : "";
116+
return `${baseUrl}/${appId}/${prefix}${endpointName}`;
110117
}
111118

112119
private buildHeaders(): Record<string, string> {
113-
return {
120+
const headers: Record<string, string> = {
114121
"X-Peek-Auth": `Bearer ${this.options.getToken()}`,
115-
"pk-api-key": this.options.gatewayKey,
116122
"Content-Type": "application/json",
117123
};
124+
if (this.options.gatewayKey) {
125+
headers["pk-api-key"] = this.options.gatewayKey;
126+
}
127+
return headers;
118128
}
119129
}

src/peek-access-service.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ import {
2222
type ProductServiceOptions,
2323
} from "./internal/products/product-service.js";
2424
import { PromoCodeService } from "./internal/promo-codes/promo-code-service.js";
25+
import { V2_EXTENDABLE_SLUG } from "./internal/gateway-endpoints.js";
2526
import { TokenManager } from "./internal/token-manager.js";
2627
import { noopLogger, type Logger } from "./logger.js";
2728

28-
/** Default backoffice GraphQL gateway base URL. */
29+
/** Default backoffice GraphQL gateway base URL (v1). */
2930
const DEFAULT_BASE_URL = "https://apps.peekapis.com/backoffice-gql";
31+
/** Default gateway base URL when operating in v2 mode. */
32+
const DEFAULT_V2_BASE_URL =
33+
"https://app-registry.peeklabs.com/installations-api";
3034
/** Default JWT lifetime (1 hour). */
3135
const DEFAULT_TOKEN_TTL_SECONDS = 3600;
3236
/** Default leeway before expiry at which a cached token is re-minted. */
@@ -44,10 +48,17 @@ export interface PeekAccessServiceConfig {
4448
issuer: string;
4549
/** Peek app ID, used in the gateway endpoint path. */
4650
appId: string;
47-
/** API gateway key, sent as the `pk-api-key` header. */
48-
gatewayKey: string;
51+
/** API gateway key, sent as the `pk-api-key` header. Required in v1 mode; not used in v2. */
52+
gatewayKey?: string;
4953

50-
/** Override the gateway base URL. Default: Peek production gateway. */
54+
/**
55+
* Gateway mode. `"v2"` routes through the app-registry installations API
56+
* (`baseUrl/appId/peek_backoffice_api-v1/endpointName`) and defaults to the
57+
* app-registry sandbox base URL. `"v1"` (default) uses the standard backoffice
58+
* GraphQL gateway.
59+
*/
60+
mode?: "v2";
61+
/** Override the gateway base URL. Default: Peek production gateway (v1) or app-registry sandbox (v2). */
5162
baseUrl?: string;
5263
/** JWT lifetime in seconds. Default: 3600. */
5364
tokenTtlSeconds?: number;
@@ -106,11 +117,12 @@ export class PeekAccessService {
106117
private reviewService?: ReviewService;
107118

108119
constructor(config: PeekAccessServiceConfig) {
120+
const isV2 = config.mode === "v2";
109121
requireNonEmpty(config.installId, "installId");
110122
requireNonEmpty(config.jwtSecret, "jwtSecret");
111123
requireNonEmpty(config.issuer, "issuer");
112124
requireNonEmpty(config.appId, "appId");
113-
requireNonEmpty(config.gatewayKey, "gatewayKey");
125+
if (!isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey");
114126

115127
const logger = config.logger ?? noopLogger;
116128
const tokens = new TokenManager({
@@ -119,17 +131,20 @@ export class PeekAccessService {
119131
installId: config.installId,
120132
ttlSeconds: config.tokenTtlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS,
121133
leewaySeconds:
122-
config.tokenRefreshLeewaySeconds ?? DEFAULT_TOKEN_REFRESH_LEEWAY_SECONDS,
134+
config.tokenRefreshLeewaySeconds ??
135+
DEFAULT_TOKEN_REFRESH_LEEWAY_SECONDS,
123136
});
124137

138+
const defaultBaseUrl = isV2 ? DEFAULT_V2_BASE_URL : DEFAULT_BASE_URL;
125139
this.client = new GraphQLClient({
126-
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
140+
baseUrl: config.baseUrl ?? defaultBaseUrl,
127141
appId: config.appId,
128142
gatewayKey: config.gatewayKey,
129143
getToken: () => tokens.getToken(),
130144
retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
131145
logger,
132146
fetchFn: config.fetch ?? globalThis.fetch,
147+
endpointPathPrefix: isV2 ? V2_EXTENDABLE_SLUG : undefined,
133148
});
134149

135150
this.productServiceOptions = {

0 commit comments

Comments
 (0)