@pymthouse/builder-sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +66 -0
  2. package/dist/{client-BroVFyIy.d.ts → client-BHfjDvIe.d.ts} +49 -1
  3. package/dist/{client-BhC1YhB1.d.cts → client-CvhJEhjV.d.cts} +49 -1
  4. package/dist/config.cjs +59 -3
  5. package/dist/config.cjs.map +1 -1
  6. package/dist/config.d.cts +8 -1
  7. package/dist/config.d.ts +8 -1
  8. package/dist/config.js +57 -4
  9. package/dist/config.js.map +1 -1
  10. package/dist/device-initiate.cjs +1 -1
  11. package/dist/device-initiate.cjs.map +1 -1
  12. package/dist/device-initiate.js +1 -1
  13. package/dist/device-initiate.js.map +1 -1
  14. package/dist/device.cjs +1 -1
  15. package/dist/device.cjs.map +1 -1
  16. package/dist/device.d.cts +1 -1
  17. package/dist/device.d.ts +1 -1
  18. package/dist/device.js +1 -1
  19. package/dist/device.js.map +1 -1
  20. package/dist/env.cjs +794 -36
  21. package/dist/env.cjs.map +1 -1
  22. package/dist/env.d.cts +2 -2
  23. package/dist/env.d.ts +2 -2
  24. package/dist/env.js +794 -36
  25. package/dist/env.js.map +1 -1
  26. package/dist/gateway/client/index.cjs +492 -0
  27. package/dist/gateway/client/index.cjs.map +1 -0
  28. package/dist/gateway/client/index.d.cts +63 -0
  29. package/dist/gateway/client/index.d.ts +63 -0
  30. package/dist/gateway/client/index.js +489 -0
  31. package/dist/gateway/client/index.js.map +1 -0
  32. package/dist/gateway/index.cjs +16 -0
  33. package/dist/gateway/index.cjs.map +1 -0
  34. package/dist/gateway/index.d.cts +52 -0
  35. package/dist/gateway/index.d.ts +52 -0
  36. package/dist/gateway/index.js +10 -0
  37. package/dist/gateway/index.js.map +1 -0
  38. package/dist/gateway/server/index.cjs +1248 -0
  39. package/dist/gateway/server/index.cjs.map +1 -0
  40. package/dist/gateway/server/index.d.cts +31 -0
  41. package/dist/gateway/server/index.d.ts +31 -0
  42. package/dist/gateway/server/index.js +1233 -0
  43. package/dist/gateway/server/index.js.map +1 -0
  44. package/dist/index.cjs +1075 -186
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +6 -4
  47. package/dist/index.d.ts +6 -4
  48. package/dist/index.js +1042 -163
  49. package/dist/index.js.map +1 -1
  50. package/dist/ingest-B3Yi8Tb1.d.cts +271 -0
  51. package/dist/ingest-DoKJTWU9.d.ts +271 -0
  52. package/dist/plan-pricing.cjs +108 -0
  53. package/dist/plan-pricing.cjs.map +1 -0
  54. package/dist/plan-pricing.d.cts +15 -0
  55. package/dist/plan-pricing.d.ts +15 -0
  56. package/dist/plan-pricing.js +98 -0
  57. package/dist/plan-pricing.js.map +1 -0
  58. package/dist/signer/server.cjs +1366 -0
  59. package/dist/signer/server.cjs.map +1 -0
  60. package/dist/signer/server.d.cts +73 -0
  61. package/dist/signer/server.d.ts +73 -0
  62. package/dist/signer/server.js +1331 -0
  63. package/dist/signer/server.js.map +1 -0
  64. package/dist/tokens.d.cts +1 -1
  65. package/dist/tokens.d.ts +1 -1
  66. package/dist/types-_R1AwEZp.d.cts +343 -0
  67. package/dist/types-_R1AwEZp.d.ts +343 -0
  68. package/dist/verify.cjs +1 -1
  69. package/dist/verify.cjs.map +1 -1
  70. package/dist/verify.d.cts +1 -1
  71. package/dist/verify.d.ts +1 -1
  72. package/dist/verify.js +1 -1
  73. package/dist/verify.js.map +1 -1
  74. package/gateway/proto/lp_rpc.proto +542 -0
  75. package/package.json +42 -1
  76. package/dist/types-rKzVXvMu.d.cts +0 -196
  77. package/dist/types-rKzVXvMu.d.ts +0 -196
package/README.md CHANGED
@@ -12,6 +12,8 @@ OAuth/OIDC protocol calls use **[oauth4webapi](https://github.com/panva/oauth4we
12
12
  pnpm add @pymthouse/builder-sdk
13
13
  ```
14
14
 
15
+ Maintainers: see [docs/RELEASING.md](docs/RELEASING.md) for trusted publishing and re-running failed releases.
16
+
15
17
  ## Quick start
16
18
 
17
19
  ```ts
@@ -66,6 +68,48 @@ const signerSession = await client.mintUserSignerSessionToken({
66
68
  For advanced flows that already have a user JWT, call
67
69
  `exchangeForSignerSession({ userJwt })` directly.
68
70
 
71
+ ### Dashboard API keys (long-lived `pmth_*`)
72
+
73
+ Create a key in the Dashboard **API keys** page, then exchange it for a signer
74
+ session without repeating device login:
75
+
76
+ ```ts
77
+ const session = await client.exchangeApiKeyForSignerSession({
78
+ apiKey: process.env.PMTH_API_KEY!,
79
+ facadeUrl: process.env.DASHBOARD_ORIGIN!, // e.g. https://dashboard.example.com
80
+ scope: "sign:job",
81
+ });
82
+ // session.access_token — opaque signer bearer for discovery / gateway
83
+ ```
84
+
85
+ See `examples/stream-with-api-key.mjs` for a minimal Node script.
86
+
87
+ ## Browser gateway (optional module)
88
+
89
+ Live Video-to-Video streaming from the browser uses a **same-origin HTTP segment relay**
90
+ implemented in optional subpaths (not exported from the main entry):
91
+
92
+ | Subpath | Use |
93
+ |---------|-----|
94
+ | `@pymthouse/builder-sdk/gateway` | Shared types |
95
+ | `@pymthouse/builder-sdk/gateway/client` | `BrowserGatewayClient` for dashboard / browser apps |
96
+ | `@pymthouse/builder-sdk/gateway/server` | Route Handler factories for Next.js |
97
+
98
+ Install peer dependencies when using the server module:
99
+
100
+ ```bash
101
+ pnpm add @grpc/grpc-js @grpc/proto-loader
102
+ ```
103
+
104
+ Auth flow (same signer bearer as Python `livepeer-python-gateway`):
105
+
106
+ 1. `exchangeApiKeyForSignerSession({ apiKey, facadeUrl: dashboardOrigin })` or `POST /api/pymthouse/keys/exchange`
107
+ 2. `Authorization: Bearer <signer_token>` on `POST /api/gateway/sessions`
108
+
109
+ Enable relay on the dashboard with `GATEWAY_ENABLED=1` and `NEXT_PUBLIC_GATEWAY_ENABLED=1`.
110
+
111
+ See `examples/gateway-session-smoke.mjs` for a headless session start test.
112
+
69
113
  Integrators can use the higher-level workflow helpers:
70
114
 
71
115
  ```ts
@@ -128,6 +172,28 @@ const summary = summarizeUsageForExternalUser(usage, externalUserId);
128
172
  // summary.requestCount, summary.feeWei (wei string)
129
173
  ```
130
174
 
175
+ ## Billing: plans, retail usage, signed-ticket ingest
176
+
177
+ **Plans (apiVersion=2):** `listBillingProducts({ apiVersion: "2" })` returns `BillingProduct[]` with capability pricing and sync status. `syncBillingProduct(planId)` POSTs to OpenMeter.
178
+
179
+ **Retail estimates:** `getUsage({ includeRetail: true, groupBy: "pipeline_model" })` adds `endUserBillableUsdMicros` / fiat rows when the active plan has retail rates.
180
+
181
+ **Signed-ticket ingest (platform metering):** after a signer proxy response, call `ingestSignedTicket` or use `forwardWithOptionalMetering` with `metering: { mode: "pymthouse_hosted" }` on `createSignerProxyServer` — usage is stripped from the client response and POSTed to `POST /api/v1/apps/{id}/usage/signed-tickets`.
182
+
183
+ **Routing:** `getSignerRouting()` returns `signerApiUrl`, `remoteDmzUrl`, `meteringMode`, and pattern hints for hosted vs platform-ingest vs BYO OpenMeter.
184
+
185
+ **Allowances (OpenMeter):** Trial and manual USD micros allowance use OpenMeter entitlements — not a Postgres wei ledger.
186
+
187
+ | Method | SDK | HTTP |
188
+ |--------|-----|------|
189
+ | Read balance | `getUsageBalance(externalUserId)` | `GET .../usage/balance?externalUserId=` |
190
+ | Read allowance detail | `getUserAllowances(externalUserId)` | `GET .../users/{id}/allowances` |
191
+ | Top-up grant | `grantUserAllowance(externalUserId, { amountUsdMicros, source })` | `POST .../users/{id}/allowances` |
192
+
193
+ `grantUserCredits` / `getUserCredits` remain as **deprecated** aliases that call the allowances / balance endpoints. `POST .../users/{id}/credits` was removed from PymtHouse (the route may still re-export allowances temporarily).
194
+
195
+ **Plan pricing helpers:** `markupPercentToRetailRateUsd`, `applyRetailRateToNetworkMicros` (exported from the main entry).
196
+
131
197
  ## Usage API: pipeline/model grouping
132
198
 
133
199
  When `getUsage({ groupBy: "pipeline_model", startDate, endDate, userId })` returns
@@ -1,5 +1,5 @@
1
1
  import { SignerSessionToken } from './tokens.js';
2
- import { m as PmtHouseClientOptions, h as GetDiscoveryOptions, O as OidcDiscoveryDocument, P as ParsedDeviceApprovalRedirect, f as AppUserRecord, n as UpsertAppUserInput, j as MintUserAccessTokenInput, k as MintUserAccessTokenResponse, D as DeviceApprovalInput, T as TokenExchangeResponse, C as ClientCredentialsTokenResponse, l as MintUserSignerSessionTokenInput, o as UsageQueryInput, b as UsageApiResponse, M as MeScopeUsagePayload, G as GetAppManifestResult, i as MintSignerSessionForExternalUserInput, g as ApproveDeviceLoginInput } from './types-rKzVXvMu.js';
2
+ import { P as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, a as ParsedDeviceApprovalRedirect, A as AppUserRecord, U as UpsertAppUserInput, M as MintUserAccessTokenInput, b as MintUserAccessTokenResponse, T as TokenExchangeResponse, D as DeviceApprovalInput, C as ClientCredentialsTokenResponse, c as MintUserSignerSessionTokenInput, d as UsageQueryInput, e as UsageApiResponse, S as SignedTicketIngestInput, f as SignedTicketIngestResult, g as SignerRoutingResponse, L as ListBillingProductsResult, h as PlanSyncResult, i as UsageBalanceResponse, j as UserAllowancesResponse, k as UserAllowanceGrantInput, l as GrantSource, m as UserSubscriptionResponse, n as MeScopeUsagePayload, o as GetAppManifestResult, p as MintSignerSessionForExternalUserInput, q as ApproveDeviceLoginInput } from './types-_R1AwEZp.js';
3
3
 
4
4
  /**
5
5
  * Normalize RFC 8628 user codes for comparison and resource URIs (uppercase, strip separators).
@@ -31,6 +31,22 @@ declare class PmtHouseClient {
31
31
  success: boolean;
32
32
  }>;
33
33
  mintUserAccessToken(input: MintUserAccessTokenInput): Promise<MintUserAccessTokenResponse>;
34
+ /**
35
+ * Exchange a long-lived dashboard API key (`pmth_*`) for a short-lived user JWT.
36
+ */
37
+ exchangeApiKeyForUserAccessToken(input: {
38
+ apiKey: string;
39
+ scope?: string;
40
+ }): Promise<MintUserAccessTokenResponse>;
41
+ /**
42
+ * Exchange a dashboard API key for a signer session via a trusted facade (recommended)
43
+ * or directly when M2M credentials are available on this client.
44
+ */
45
+ exchangeApiKeyForSignerSession(input: {
46
+ apiKey: string;
47
+ scope?: string;
48
+ facadeUrl?: string;
49
+ }): Promise<TokenExchangeResponse>;
34
50
  completeDeviceApproval(input: DeviceApprovalInput): Promise<TokenExchangeResponse>;
35
51
  issueMachineAccessToken(scope?: string): Promise<ClientCredentialsTokenResponse>;
36
52
  exchangeForSignerSession(input: {
@@ -49,6 +65,38 @@ declare class PmtHouseClient {
49
65
  /**
50
66
  * Session-scoped usage for one `externalUserId`: user rollup plus merged pipeline/model breakdown.
51
67
  */
68
+ ingestSignedTicket(ticket: SignedTicketIngestInput): Promise<SignedTicketIngestResult>;
69
+ ingestSignedTickets(tickets: SignedTicketIngestInput[]): Promise<{
70
+ results: Array<SignedTicketIngestResult & {
71
+ requestId?: string;
72
+ ok?: boolean;
73
+ }>;
74
+ }>;
75
+ getSignerRouting(): Promise<SignerRoutingResponse>;
76
+ listBillingProducts(): Promise<ListBillingProductsResult>;
77
+ syncBillingProduct(planId: string): Promise<PlanSyncResult>;
78
+ getUsageBalance(externalUserId: string): Promise<UsageBalanceResponse>;
79
+ getUserAllowances(externalUserId: string): Promise<UserAllowancesResponse>;
80
+ grantUserAllowance(externalUserId: string, input: UserAllowanceGrantInput): Promise<UserAllowancesResponse & {
81
+ grantedUsdMicros?: string;
82
+ featureKey?: string;
83
+ }>;
84
+ /**
85
+ * @deprecated Removed from PymtHouse — use {@link getUsageBalance} or {@link getUserAllowances}.
86
+ */
87
+ getUserCredits(externalUserId: string): Promise<UsageBalanceResponse>;
88
+ /**
89
+ * @deprecated Removed from PymtHouse — use {@link grantUserAllowance} (`POST .../allowances`).
90
+ */
91
+ grantUserCredits(externalUserId: string, input: {
92
+ amountUsdMicros: string;
93
+ source?: GrantSource;
94
+ featureKey?: string;
95
+ }): Promise<UsageBalanceResponse & {
96
+ grantedUsdMicros?: string;
97
+ featureKey?: string;
98
+ }>;
99
+ getUserSubscription(externalUserId: string): Promise<UserSubscriptionResponse>;
52
100
  fetchUsageForExternalUser(input: {
53
101
  externalUserId: string;
54
102
  startDate: string;
@@ -1,5 +1,5 @@
1
1
  import { SignerSessionToken } from './tokens.cjs';
2
- import { m as PmtHouseClientOptions, h as GetDiscoveryOptions, O as OidcDiscoveryDocument, P as ParsedDeviceApprovalRedirect, f as AppUserRecord, n as UpsertAppUserInput, j as MintUserAccessTokenInput, k as MintUserAccessTokenResponse, D as DeviceApprovalInput, T as TokenExchangeResponse, C as ClientCredentialsTokenResponse, l as MintUserSignerSessionTokenInput, o as UsageQueryInput, b as UsageApiResponse, M as MeScopeUsagePayload, G as GetAppManifestResult, i as MintSignerSessionForExternalUserInput, g as ApproveDeviceLoginInput } from './types-rKzVXvMu.cjs';
2
+ import { P as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, a as ParsedDeviceApprovalRedirect, A as AppUserRecord, U as UpsertAppUserInput, M as MintUserAccessTokenInput, b as MintUserAccessTokenResponse, T as TokenExchangeResponse, D as DeviceApprovalInput, C as ClientCredentialsTokenResponse, c as MintUserSignerSessionTokenInput, d as UsageQueryInput, e as UsageApiResponse, S as SignedTicketIngestInput, f as SignedTicketIngestResult, g as SignerRoutingResponse, L as ListBillingProductsResult, h as PlanSyncResult, i as UsageBalanceResponse, j as UserAllowancesResponse, k as UserAllowanceGrantInput, l as GrantSource, m as UserSubscriptionResponse, n as MeScopeUsagePayload, o as GetAppManifestResult, p as MintSignerSessionForExternalUserInput, q as ApproveDeviceLoginInput } from './types-_R1AwEZp.cjs';
3
3
 
4
4
  /**
5
5
  * Normalize RFC 8628 user codes for comparison and resource URIs (uppercase, strip separators).
@@ -31,6 +31,22 @@ declare class PmtHouseClient {
31
31
  success: boolean;
32
32
  }>;
33
33
  mintUserAccessToken(input: MintUserAccessTokenInput): Promise<MintUserAccessTokenResponse>;
34
+ /**
35
+ * Exchange a long-lived dashboard API key (`pmth_*`) for a short-lived user JWT.
36
+ */
37
+ exchangeApiKeyForUserAccessToken(input: {
38
+ apiKey: string;
39
+ scope?: string;
40
+ }): Promise<MintUserAccessTokenResponse>;
41
+ /**
42
+ * Exchange a dashboard API key for a signer session via a trusted facade (recommended)
43
+ * or directly when M2M credentials are available on this client.
44
+ */
45
+ exchangeApiKeyForSignerSession(input: {
46
+ apiKey: string;
47
+ scope?: string;
48
+ facadeUrl?: string;
49
+ }): Promise<TokenExchangeResponse>;
34
50
  completeDeviceApproval(input: DeviceApprovalInput): Promise<TokenExchangeResponse>;
35
51
  issueMachineAccessToken(scope?: string): Promise<ClientCredentialsTokenResponse>;
36
52
  exchangeForSignerSession(input: {
@@ -49,6 +65,38 @@ declare class PmtHouseClient {
49
65
  /**
50
66
  * Session-scoped usage for one `externalUserId`: user rollup plus merged pipeline/model breakdown.
51
67
  */
68
+ ingestSignedTicket(ticket: SignedTicketIngestInput): Promise<SignedTicketIngestResult>;
69
+ ingestSignedTickets(tickets: SignedTicketIngestInput[]): Promise<{
70
+ results: Array<SignedTicketIngestResult & {
71
+ requestId?: string;
72
+ ok?: boolean;
73
+ }>;
74
+ }>;
75
+ getSignerRouting(): Promise<SignerRoutingResponse>;
76
+ listBillingProducts(): Promise<ListBillingProductsResult>;
77
+ syncBillingProduct(planId: string): Promise<PlanSyncResult>;
78
+ getUsageBalance(externalUserId: string): Promise<UsageBalanceResponse>;
79
+ getUserAllowances(externalUserId: string): Promise<UserAllowancesResponse>;
80
+ grantUserAllowance(externalUserId: string, input: UserAllowanceGrantInput): Promise<UserAllowancesResponse & {
81
+ grantedUsdMicros?: string;
82
+ featureKey?: string;
83
+ }>;
84
+ /**
85
+ * @deprecated Removed from PymtHouse — use {@link getUsageBalance} or {@link getUserAllowances}.
86
+ */
87
+ getUserCredits(externalUserId: string): Promise<UsageBalanceResponse>;
88
+ /**
89
+ * @deprecated Removed from PymtHouse — use {@link grantUserAllowance} (`POST .../allowances`).
90
+ */
91
+ grantUserCredits(externalUserId: string, input: {
92
+ amountUsdMicros: string;
93
+ source?: GrantSource;
94
+ featureKey?: string;
95
+ }): Promise<UsageBalanceResponse & {
96
+ grantedUsdMicros?: string;
97
+ featureKey?: string;
98
+ }>;
99
+ getUserSubscription(externalUserId: string): Promise<UserSubscriptionResponse>;
52
100
  fetchUsageForExternalUser(input: {
53
101
  externalUserId: string;
54
102
  startDate: string;
package/dist/config.cjs CHANGED
@@ -3,11 +3,65 @@
3
3
  // src/string-utils.ts
4
4
  function stripTrailingSlashes(value) {
5
5
  let end = value.length;
6
- while (end > 0 && value.charCodeAt(end - 1) === 47) {
6
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
7
7
  end--;
8
8
  }
9
9
  return value.slice(0, end);
10
10
  }
11
+ function endsWithIgnoreCase(value, suffix) {
12
+ if (suffix.length > value.length) {
13
+ return false;
14
+ }
15
+ const start = value.length - suffix.length;
16
+ for (let i = 0; i < suffix.length; i++) {
17
+ const a = value.codePointAt(start + i) ?? 0;
18
+ const b = suffix.codePointAt(i) ?? 0;
19
+ if (a !== b && (a | 32) !== (b | 32)) {
20
+ return false;
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+ function stripSuffixIgnoreCase(value, suffix) {
26
+ return endsWithIgnoreCase(value, suffix) ? value.slice(0, value.length - suffix.length) : value;
27
+ }
28
+ function stripOidcPathSuffix(issuerUrl) {
29
+ let base = stripTrailingSlashes(issuerUrl.trim());
30
+ base = stripSuffixIgnoreCase(base, "/oidc");
31
+ return stripTrailingSlashes(base);
32
+ }
33
+ function isSafePathSegment(value) {
34
+ if (typeof value !== "string" || value.length === 0 || value.length > 128) {
35
+ return false;
36
+ }
37
+ for (let i = 0; i < value.length; i++) {
38
+ const c = value.codePointAt(i) ?? 0;
39
+ const ok = c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 45;
40
+ if (!ok) {
41
+ return false;
42
+ }
43
+ }
44
+ return true;
45
+ }
46
+ function parseHttpOrigin(raw, fallback) {
47
+ const trimmed = (raw ?? fallback).trim();
48
+ let parsed;
49
+ try {
50
+ parsed = new URL(trimmed);
51
+ } catch {
52
+ throw new TypeError("Origin must be a valid http(s) URL");
53
+ }
54
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
55
+ throw new TypeError("Origin must use http or https");
56
+ }
57
+ return parsed.origin;
58
+ }
59
+ function buildGatewaySessionDeleteUrl(origin, sessionId) {
60
+ if (!isSafePathSegment(sessionId)) {
61
+ throw new TypeError("Invalid gateway session id");
62
+ }
63
+ return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);
64
+ }
11
65
 
12
66
  // src/config.ts
13
67
  var PYMTHOUSE_NOT_CONFIGURED_MESSAGE = "PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.";
@@ -48,19 +102,21 @@ function isPymthouseConfigured() {
48
102
  return readPymthouseEnv() !== null;
49
103
  }
50
104
  function getBuilderApiV1BaseFromIssuerUrl(issuerUrl) {
51
- const noTrail = stripTrailingSlashes(issuerUrl.trim());
52
- return noTrail.replace(/\/oidc\/?$/i, "");
105
+ return stripOidcPathSuffix(issuerUrl);
53
106
  }
54
107
  function getPymthouseIssuerOrigin(issuerUrl) {
55
108
  return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;
56
109
  }
57
110
 
58
111
  exports.PYMTHOUSE_NOT_CONFIGURED_MESSAGE = PYMTHOUSE_NOT_CONFIGURED_MESSAGE;
112
+ exports.buildGatewaySessionDeleteUrl = buildGatewaySessionDeleteUrl;
59
113
  exports.getBuilderApiV1BaseFromIssuerUrl = getBuilderApiV1BaseFromIssuerUrl;
60
114
  exports.getPymthouseIssuerOrigin = getPymthouseIssuerOrigin;
61
115
  exports.getPymthouseIssuerUrlFromEnv = getPymthouseIssuerUrlFromEnv;
62
116
  exports.getPymthousePublicClientIdFromEnv = getPymthousePublicClientIdFromEnv;
63
117
  exports.isPymthouseConfigured = isPymthouseConfigured;
118
+ exports.isSafePathSegment = isSafePathSegment;
119
+ exports.parseHttpOrigin = parseHttpOrigin;
64
120
  exports.readPymthouseEnv = readPymthouseEnv;
65
121
  //# sourceMappingURL=config.cjs.map
66
122
  //# sourceMappingURL=config.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/string-utils.ts","../src/config.ts"],"names":[],"mappings":";;;AACO,SAAS,qBAAqB,KAAA,EAAuB;AAC1D,EAAA,IAAI,MAAM,KAAA,CAAM,MAAA;AAChB,EAAA,OAAO,MAAM,CAAA,IAAK,KAAA,CAAM,WAAW,GAAA,GAAM,CAAC,MAAM,EAAA,EAAI;AAClD,IAAA,GAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC3B;;;ACJO,IAAM,gCAAA,GACX;AASF,SAAS,QAAQ,IAAA,EAA6B;AAC5C,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA;AAC9B,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,OAAO,OAAA,IAAW,IAAA;AACpB;AAGO,SAAS,gBAAA,GAA8C;AAC5D,EAAA,MAAM,SAAA,GAAY,QAAQ,sBAAsB,CAAA;AAChD,EAAA,MAAM,cAAA,GAAiB,QAAQ,4BAA4B,CAAA;AAC3D,EAAA,MAAM,WAAA,GAAc,QAAQ,yBAAyB,CAAA;AACrD,EAAA,MAAM,eAAA,GAAkB,QAAQ,6BAA6B,CAAA;AAC7D,EAAA,IAAI,CAAC,SAAA,IAAa,CAAC,kBAAkB,CAAC,WAAA,IAAe,CAAC,eAAA,EAAiB;AACrE,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,qBAAqB,SAAS,CAAA;AAAA,IACzC,cAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF;AAGO,SAAS,4BAAA,GAA8C;AAC5D,EAAA,MAAM,GAAA,GAAM,QAAQ,sBAAsB,CAAA;AAC1C,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,IAAI;AACF,IAAA,OAAO,oBAAA,CAAqB,IAAI,GAAA,CAAI,GAAG,EAAE,IAAI,CAAA;AAAA,EAC/C,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGO,SAAS,iCAAA,GAAmD;AACjE,EAAA,OAAO,QAAQ,4BAA4B,CAAA;AAC7C;AAGO,SAAS,qBAAA,GAAiC;AAC/C,EAAA,OAAO,kBAAiB,KAAM,IAAA;AAChC;AAGO,SAAS,iCAAiC,SAAA,EAA2B;AAC1E,EAAA,MAAM,OAAA,GAAU,oBAAA,CAAqB,SAAA,CAAU,IAAA,EAAM,CAAA;AACrD,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAC1C;AAGO,SAAS,yBAAyB,SAAA,EAA2B;AAClE,EAAA,OAAO,IAAI,GAAA,CAAI,oBAAA,CAAqB,UAAU,IAAA,EAAM,CAAC,CAAA,CAAE,MAAA;AACzD","file":"config.cjs","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n","import { stripTrailingSlashes } from \"./string-utils.js\";\n\n/** Operator hint when Builder / Usage cannot run. */\nexport const PYMTHOUSE_NOT_CONFIGURED_MESSAGE =\n \"PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.\";\n\nexport interface PymthouseEnvConfig {\n issuerUrl: string;\n publicClientId: string;\n m2mClientId: string;\n m2mClientSecret: string;\n}\n\nfunction trimEnv(name: string): string | null {\n const value = process.env[name];\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed || null;\n}\n\n/** Read `PYMTHOUSE_*` env vars without throwing. Returns null when incomplete. */\nexport function readPymthouseEnv(): PymthouseEnvConfig | null {\n const issuerUrl = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n const publicClientId = trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n const m2mClientId = trimEnv(\"PYMTHOUSE_M2M_CLIENT_ID\");\n const m2mClientSecret = trimEnv(\"PYMTHOUSE_M2M_CLIENT_SECRET\");\n if (!issuerUrl || !publicClientId || !m2mClientId || !m2mClientSecret) {\n return null;\n }\n return {\n issuerUrl: stripTrailingSlashes(issuerUrl),\n publicClientId,\n m2mClientId,\n m2mClientSecret,\n };\n}\n\n/** Read `PYMTHOUSE_ISSUER_URL` without requiring full M2M configuration. */\nexport function getPymthouseIssuerUrlFromEnv(): string | null {\n const raw = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n if (!raw) return null;\n try {\n return stripTrailingSlashes(new URL(raw).href);\n } catch {\n return null;\n }\n}\n\n/** Read `PYMTHOUSE_PUBLIC_CLIENT_ID` without requiring full M2M configuration. */\nexport function getPymthousePublicClientIdFromEnv(): string | null {\n return trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n}\n\n/** True when all vars required by `createPmtHouseClientFromEnv` are present. */\nexport function isPymthouseConfigured(): boolean {\n return readPymthouseEnv() !== null;\n}\n\n/** Resolve Builder API base (`…/api/v1`) from issuer URL (`…/api/v1/oidc`). */\nexport function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string {\n const noTrail = stripTrailingSlashes(issuerUrl.trim());\n return noTrail.replace(/\\/oidc\\/?$/i, \"\");\n}\n\n/** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */\nexport function getPymthouseIssuerOrigin(issuerUrl: string): string {\n return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;\n}\n"]}
1
+ {"version":3,"sources":["../src/string-utils.ts","../src/config.ts"],"names":[],"mappings":"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","file":"config.cjs","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n\nfunction endsWithIgnoreCase(value: string, suffix: string): boolean {\n if (suffix.length > value.length) {\n return false;\n }\n const start = value.length - suffix.length;\n for (let i = 0; i < suffix.length; i++) {\n const a = value.codePointAt(start + i) ?? 0;\n const b = suffix.codePointAt(i) ?? 0;\n if (a !== b && (a | 32) !== (b | 32)) {\n return false;\n }\n }\n return true;\n}\n\nfunction stripSuffixIgnoreCase(value: string, suffix: string): string {\n return endsWithIgnoreCase(value, suffix)\n ? value.slice(0, value.length - suffix.length)\n : value;\n}\n\n/** Issuer URL (`…/oidc`) → Builder API base (`…/api/v1`). Linear-time; no regex. */\nexport function stripOidcPathSuffix(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Issuer URL (`…/api/v1/oidc`) → host origin for signer/API-key routes. Linear-time; no regex. */\nexport function stripIssuerOriginFromOidcUrl(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/api/v1/oidc\");\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Validate gateway session ids before embedding in request URLs. */\nexport function isSafePathSegment(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 128) {\n return false;\n }\n for (let i = 0; i < value.length; i++) {\n const c = value.codePointAt(i) ?? 0;\n const ok =\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45;\n if (!ok) {\n return false;\n }\n }\n return true;\n}\n\n/** Parse and validate an http(s) facade origin (no path). */\nexport function parseHttpOrigin(raw: string | undefined, fallback: string): string {\n const trimmed = (raw ?? fallback).trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new TypeError(\"Origin must be a valid http(s) URL\");\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new TypeError(\"Origin must use http or https\");\n }\n return parsed.origin;\n}\n\n/** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */\nexport function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL {\n if (!isSafePathSegment(sessionId)) {\n throw new TypeError(\"Invalid gateway session id\");\n }\n return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);\n}\n","import {\n buildGatewaySessionDeleteUrl,\n isSafePathSegment,\n parseHttpOrigin,\n stripOidcPathSuffix,\n stripTrailingSlashes,\n} from \"./string-utils.js\";\n\nexport { buildGatewaySessionDeleteUrl, isSafePathSegment, parseHttpOrigin };\n\n/** Operator hint when Builder / Usage cannot run. */\nexport const PYMTHOUSE_NOT_CONFIGURED_MESSAGE =\n \"PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.\";\n\nexport interface PymthouseEnvConfig {\n issuerUrl: string;\n publicClientId: string;\n m2mClientId: string;\n m2mClientSecret: string;\n}\n\nfunction trimEnv(name: string): string | null {\n const value = process.env[name];\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed || null;\n}\n\n/** Read `PYMTHOUSE_*` env vars without throwing. Returns null when incomplete. */\nexport function readPymthouseEnv(): PymthouseEnvConfig | null {\n const issuerUrl = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n const publicClientId = trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n const m2mClientId = trimEnv(\"PYMTHOUSE_M2M_CLIENT_ID\");\n const m2mClientSecret = trimEnv(\"PYMTHOUSE_M2M_CLIENT_SECRET\");\n if (!issuerUrl || !publicClientId || !m2mClientId || !m2mClientSecret) {\n return null;\n }\n return {\n issuerUrl: stripTrailingSlashes(issuerUrl),\n publicClientId,\n m2mClientId,\n m2mClientSecret,\n };\n}\n\n/** Read `PYMTHOUSE_ISSUER_URL` without requiring full M2M configuration. */\nexport function getPymthouseIssuerUrlFromEnv(): string | null {\n const raw = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n if (!raw) return null;\n try {\n return stripTrailingSlashes(new URL(raw).href);\n } catch {\n return null;\n }\n}\n\n/** Read `PYMTHOUSE_PUBLIC_CLIENT_ID` without requiring full M2M configuration. */\nexport function getPymthousePublicClientIdFromEnv(): string | null {\n return trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n}\n\n/** True when all vars required by `createPmtHouseClientFromEnv` are present. */\nexport function isPymthouseConfigured(): boolean {\n return readPymthouseEnv() !== null;\n}\n\n/** Resolve Builder API base (`…/api/v1`) from issuer URL (`…/api/v1/oidc`). */\nexport function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string {\n return stripOidcPathSuffix(issuerUrl);\n}\n\n/** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */\nexport function getPymthouseIssuerOrigin(issuerUrl: string): string {\n return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;\n}\n"]}
package/dist/config.d.cts CHANGED
@@ -1,3 +1,10 @@
1
+ /** Validate gateway session ids before embedding in request URLs. */
2
+ declare function isSafePathSegment(value: unknown): value is string;
3
+ /** Parse and validate an http(s) facade origin (no path). */
4
+ declare function parseHttpOrigin(raw: string | undefined, fallback: string): string;
5
+ /** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */
6
+ declare function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL;
7
+
1
8
  /** Operator hint when Builder / Usage cannot run. */
2
9
  declare const PYMTHOUSE_NOT_CONFIGURED_MESSAGE = "PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.";
3
10
  interface PymthouseEnvConfig {
@@ -19,4 +26,4 @@ declare function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string;
19
26
  /** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */
20
27
  declare function getPymthouseIssuerOrigin(issuerUrl: string): string;
21
28
 
22
- export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, type PymthouseEnvConfig, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, readPymthouseEnv };
29
+ export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, type PymthouseEnvConfig, buildGatewaySessionDeleteUrl, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, isSafePathSegment, parseHttpOrigin, readPymthouseEnv };
package/dist/config.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /** Validate gateway session ids before embedding in request URLs. */
2
+ declare function isSafePathSegment(value: unknown): value is string;
3
+ /** Parse and validate an http(s) facade origin (no path). */
4
+ declare function parseHttpOrigin(raw: string | undefined, fallback: string): string;
5
+ /** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */
6
+ declare function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL;
7
+
1
8
  /** Operator hint when Builder / Usage cannot run. */
2
9
  declare const PYMTHOUSE_NOT_CONFIGURED_MESSAGE = "PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.";
3
10
  interface PymthouseEnvConfig {
@@ -19,4 +26,4 @@ declare function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string;
19
26
  /** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */
20
27
  declare function getPymthouseIssuerOrigin(issuerUrl: string): string;
21
28
 
22
- export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, type PymthouseEnvConfig, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, readPymthouseEnv };
29
+ export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, type PymthouseEnvConfig, buildGatewaySessionDeleteUrl, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, isSafePathSegment, parseHttpOrigin, readPymthouseEnv };
package/dist/config.js CHANGED
@@ -1,11 +1,65 @@
1
1
  // src/string-utils.ts
2
2
  function stripTrailingSlashes(value) {
3
3
  let end = value.length;
4
- while (end > 0 && value.charCodeAt(end - 1) === 47) {
4
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
5
5
  end--;
6
6
  }
7
7
  return value.slice(0, end);
8
8
  }
9
+ function endsWithIgnoreCase(value, suffix) {
10
+ if (suffix.length > value.length) {
11
+ return false;
12
+ }
13
+ const start = value.length - suffix.length;
14
+ for (let i = 0; i < suffix.length; i++) {
15
+ const a = value.codePointAt(start + i) ?? 0;
16
+ const b = suffix.codePointAt(i) ?? 0;
17
+ if (a !== b && (a | 32) !== (b | 32)) {
18
+ return false;
19
+ }
20
+ }
21
+ return true;
22
+ }
23
+ function stripSuffixIgnoreCase(value, suffix) {
24
+ return endsWithIgnoreCase(value, suffix) ? value.slice(0, value.length - suffix.length) : value;
25
+ }
26
+ function stripOidcPathSuffix(issuerUrl) {
27
+ let base = stripTrailingSlashes(issuerUrl.trim());
28
+ base = stripSuffixIgnoreCase(base, "/oidc");
29
+ return stripTrailingSlashes(base);
30
+ }
31
+ function isSafePathSegment(value) {
32
+ if (typeof value !== "string" || value.length === 0 || value.length > 128) {
33
+ return false;
34
+ }
35
+ for (let i = 0; i < value.length; i++) {
36
+ const c = value.codePointAt(i) ?? 0;
37
+ const ok = c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 45;
38
+ if (!ok) {
39
+ return false;
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+ function parseHttpOrigin(raw, fallback) {
45
+ const trimmed = (raw ?? fallback).trim();
46
+ let parsed;
47
+ try {
48
+ parsed = new URL(trimmed);
49
+ } catch {
50
+ throw new TypeError("Origin must be a valid http(s) URL");
51
+ }
52
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
53
+ throw new TypeError("Origin must use http or https");
54
+ }
55
+ return parsed.origin;
56
+ }
57
+ function buildGatewaySessionDeleteUrl(origin, sessionId) {
58
+ if (!isSafePathSegment(sessionId)) {
59
+ throw new TypeError("Invalid gateway session id");
60
+ }
61
+ return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);
62
+ }
9
63
 
10
64
  // src/config.ts
11
65
  var PYMTHOUSE_NOT_CONFIGURED_MESSAGE = "PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.";
@@ -46,13 +100,12 @@ function isPymthouseConfigured() {
46
100
  return readPymthouseEnv() !== null;
47
101
  }
48
102
  function getBuilderApiV1BaseFromIssuerUrl(issuerUrl) {
49
- const noTrail = stripTrailingSlashes(issuerUrl.trim());
50
- return noTrail.replace(/\/oidc\/?$/i, "");
103
+ return stripOidcPathSuffix(issuerUrl);
51
104
  }
52
105
  function getPymthouseIssuerOrigin(issuerUrl) {
53
106
  return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;
54
107
  }
55
108
 
56
- export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, readPymthouseEnv };
109
+ export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, buildGatewaySessionDeleteUrl, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, isSafePathSegment, parseHttpOrigin, readPymthouseEnv };
57
110
  //# sourceMappingURL=config.js.map
58
111
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/string-utils.ts","../src/config.ts"],"names":[],"mappings":";AACO,SAAS,qBAAqB,KAAA,EAAuB;AAC1D,EAAA,IAAI,MAAM,KAAA,CAAM,MAAA;AAChB,EAAA,OAAO,MAAM,CAAA,IAAK,KAAA,CAAM,WAAW,GAAA,GAAM,CAAC,MAAM,EAAA,EAAI;AAClD,IAAA,GAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC3B;;;ACJO,IAAM,gCAAA,GACX;AASF,SAAS,QAAQ,IAAA,EAA6B;AAC5C,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA;AAC9B,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,OAAO,OAAA,IAAW,IAAA;AACpB;AAGO,SAAS,gBAAA,GAA8C;AAC5D,EAAA,MAAM,SAAA,GAAY,QAAQ,sBAAsB,CAAA;AAChD,EAAA,MAAM,cAAA,GAAiB,QAAQ,4BAA4B,CAAA;AAC3D,EAAA,MAAM,WAAA,GAAc,QAAQ,yBAAyB,CAAA;AACrD,EAAA,MAAM,eAAA,GAAkB,QAAQ,6BAA6B,CAAA;AAC7D,EAAA,IAAI,CAAC,SAAA,IAAa,CAAC,kBAAkB,CAAC,WAAA,IAAe,CAAC,eAAA,EAAiB;AACrE,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,qBAAqB,SAAS,CAAA;AAAA,IACzC,cAAA;AAAA,IACA,WAAA;AAAA,IACA;AAAA,GACF;AACF;AAGO,SAAS,4BAAA,GAA8C;AAC5D,EAAA,MAAM,GAAA,GAAM,QAAQ,sBAAsB,CAAA;AAC1C,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,IAAI;AACF,IAAA,OAAO,oBAAA,CAAqB,IAAI,GAAA,CAAI,GAAG,EAAE,IAAI,CAAA;AAAA,EAC/C,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGO,SAAS,iCAAA,GAAmD;AACjE,EAAA,OAAO,QAAQ,4BAA4B,CAAA;AAC7C;AAGO,SAAS,qBAAA,GAAiC;AAC/C,EAAA,OAAO,kBAAiB,KAAM,IAAA;AAChC;AAGO,SAAS,iCAAiC,SAAA,EAA2B;AAC1E,EAAA,MAAM,OAAA,GAAU,oBAAA,CAAqB,SAAA,CAAU,IAAA,EAAM,CAAA;AACrD,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA;AAC1C;AAGO,SAAS,yBAAyB,SAAA,EAA2B;AAClE,EAAA,OAAO,IAAI,GAAA,CAAI,oBAAA,CAAqB,UAAU,IAAA,EAAM,CAAC,CAAA,CAAE,MAAA;AACzD","file":"config.js","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n","import { stripTrailingSlashes } from \"./string-utils.js\";\n\n/** Operator hint when Builder / Usage cannot run. */\nexport const PYMTHOUSE_NOT_CONFIGURED_MESSAGE =\n \"PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.\";\n\nexport interface PymthouseEnvConfig {\n issuerUrl: string;\n publicClientId: string;\n m2mClientId: string;\n m2mClientSecret: string;\n}\n\nfunction trimEnv(name: string): string | null {\n const value = process.env[name];\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed || null;\n}\n\n/** Read `PYMTHOUSE_*` env vars without throwing. Returns null when incomplete. */\nexport function readPymthouseEnv(): PymthouseEnvConfig | null {\n const issuerUrl = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n const publicClientId = trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n const m2mClientId = trimEnv(\"PYMTHOUSE_M2M_CLIENT_ID\");\n const m2mClientSecret = trimEnv(\"PYMTHOUSE_M2M_CLIENT_SECRET\");\n if (!issuerUrl || !publicClientId || !m2mClientId || !m2mClientSecret) {\n return null;\n }\n return {\n issuerUrl: stripTrailingSlashes(issuerUrl),\n publicClientId,\n m2mClientId,\n m2mClientSecret,\n };\n}\n\n/** Read `PYMTHOUSE_ISSUER_URL` without requiring full M2M configuration. */\nexport function getPymthouseIssuerUrlFromEnv(): string | null {\n const raw = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n if (!raw) return null;\n try {\n return stripTrailingSlashes(new URL(raw).href);\n } catch {\n return null;\n }\n}\n\n/** Read `PYMTHOUSE_PUBLIC_CLIENT_ID` without requiring full M2M configuration. */\nexport function getPymthousePublicClientIdFromEnv(): string | null {\n return trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n}\n\n/** True when all vars required by `createPmtHouseClientFromEnv` are present. */\nexport function isPymthouseConfigured(): boolean {\n return readPymthouseEnv() !== null;\n}\n\n/** Resolve Builder API base (`…/api/v1`) from issuer URL (`…/api/v1/oidc`). */\nexport function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string {\n const noTrail = stripTrailingSlashes(issuerUrl.trim());\n return noTrail.replace(/\\/oidc\\/?$/i, \"\");\n}\n\n/** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */\nexport function getPymthouseIssuerOrigin(issuerUrl: string): string {\n return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;\n}\n"]}
1
+ {"version":3,"sources":["../src/string-utils.ts","../src/config.ts"],"names":[],"mappings":"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","file":"config.js","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n\nfunction endsWithIgnoreCase(value: string, suffix: string): boolean {\n if (suffix.length > value.length) {\n return false;\n }\n const start = value.length - suffix.length;\n for (let i = 0; i < suffix.length; i++) {\n const a = value.codePointAt(start + i) ?? 0;\n const b = suffix.codePointAt(i) ?? 0;\n if (a !== b && (a | 32) !== (b | 32)) {\n return false;\n }\n }\n return true;\n}\n\nfunction stripSuffixIgnoreCase(value: string, suffix: string): string {\n return endsWithIgnoreCase(value, suffix)\n ? value.slice(0, value.length - suffix.length)\n : value;\n}\n\n/** Issuer URL (`…/oidc`) → Builder API base (`…/api/v1`). Linear-time; no regex. */\nexport function stripOidcPathSuffix(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Issuer URL (`…/api/v1/oidc`) → host origin for signer/API-key routes. Linear-time; no regex. */\nexport function stripIssuerOriginFromOidcUrl(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/api/v1/oidc\");\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Validate gateway session ids before embedding in request URLs. */\nexport function isSafePathSegment(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 128) {\n return false;\n }\n for (let i = 0; i < value.length; i++) {\n const c = value.codePointAt(i) ?? 0;\n const ok =\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45;\n if (!ok) {\n return false;\n }\n }\n return true;\n}\n\n/** Parse and validate an http(s) facade origin (no path). */\nexport function parseHttpOrigin(raw: string | undefined, fallback: string): string {\n const trimmed = (raw ?? fallback).trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new TypeError(\"Origin must be a valid http(s) URL\");\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new TypeError(\"Origin must use http or https\");\n }\n return parsed.origin;\n}\n\n/** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */\nexport function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL {\n if (!isSafePathSegment(sessionId)) {\n throw new TypeError(\"Invalid gateway session id\");\n }\n return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);\n}\n","import {\n buildGatewaySessionDeleteUrl,\n isSafePathSegment,\n parseHttpOrigin,\n stripOidcPathSuffix,\n stripTrailingSlashes,\n} from \"./string-utils.js\";\n\nexport { buildGatewaySessionDeleteUrl, isSafePathSegment, parseHttpOrigin };\n\n/** Operator hint when Builder / Usage cannot run. */\nexport const PYMTHOUSE_NOT_CONFIGURED_MESSAGE =\n \"PymtHouse is not configured. Set PYMTHOUSE_ISSUER_URL, PYMTHOUSE_PUBLIC_CLIENT_ID, PYMTHOUSE_M2M_CLIENT_ID, and PYMTHOUSE_M2M_CLIENT_SECRET, then restart.\";\n\nexport interface PymthouseEnvConfig {\n issuerUrl: string;\n publicClientId: string;\n m2mClientId: string;\n m2mClientSecret: string;\n}\n\nfunction trimEnv(name: string): string | null {\n const value = process.env[name];\n if (!value) return null;\n const trimmed = value.trim();\n return trimmed || null;\n}\n\n/** Read `PYMTHOUSE_*` env vars without throwing. Returns null when incomplete. */\nexport function readPymthouseEnv(): PymthouseEnvConfig | null {\n const issuerUrl = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n const publicClientId = trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n const m2mClientId = trimEnv(\"PYMTHOUSE_M2M_CLIENT_ID\");\n const m2mClientSecret = trimEnv(\"PYMTHOUSE_M2M_CLIENT_SECRET\");\n if (!issuerUrl || !publicClientId || !m2mClientId || !m2mClientSecret) {\n return null;\n }\n return {\n issuerUrl: stripTrailingSlashes(issuerUrl),\n publicClientId,\n m2mClientId,\n m2mClientSecret,\n };\n}\n\n/** Read `PYMTHOUSE_ISSUER_URL` without requiring full M2M configuration. */\nexport function getPymthouseIssuerUrlFromEnv(): string | null {\n const raw = trimEnv(\"PYMTHOUSE_ISSUER_URL\");\n if (!raw) return null;\n try {\n return stripTrailingSlashes(new URL(raw).href);\n } catch {\n return null;\n }\n}\n\n/** Read `PYMTHOUSE_PUBLIC_CLIENT_ID` without requiring full M2M configuration. */\nexport function getPymthousePublicClientIdFromEnv(): string | null {\n return trimEnv(\"PYMTHOUSE_PUBLIC_CLIENT_ID\");\n}\n\n/** True when all vars required by `createPmtHouseClientFromEnv` are present. */\nexport function isPymthouseConfigured(): boolean {\n return readPymthouseEnv() !== null;\n}\n\n/** Resolve Builder API base (`…/api/v1`) from issuer URL (`…/api/v1/oidc`). */\nexport function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string {\n return stripOidcPathSuffix(issuerUrl);\n}\n\n/** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */\nexport function getPymthouseIssuerOrigin(issuerUrl: string): string {\n return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  // src/string-utils.ts
4
4
  function stripTrailingSlashes(value) {
5
5
  let end = value.length;
6
- while (end > 0 && value.charCodeAt(end - 1) === 47) {
6
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
7
7
  end--;
8
8
  }
9
9
  return value.slice(0, end);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/string-utils.ts","../src/device-initiate.ts"],"names":[],"mappings":"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","file":"device-initiate.cjs","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n","import { stripTrailingSlashes } from \"./string-utils.js\";\n\n/** RFC 8628 user codes: 4–16 chars, at least one alphanumeric (not all dashes). */\nexport const USER_CODE_RE = /^(?=.*[A-Z0-9])[A-Z0-9-]{4,16}$/;\n\nexport type ValidateDeviceInitiateResult =\n | { ok: true; returnUrl: string }\n | { ok: false; reason: string };\n\nexport type DeviceApprovalTuple =\n | { userCode: string; publicClientId: string }\n | { error: string };\n\nfunction normalizeIssuerUrl(iss: string): string {\n try {\n return stripTrailingSlashes(new URL(iss.trim()).href);\n } catch {\n return iss.trim();\n }\n}\n\n/**\n * Validate OP-issued `iss` + `target_link_uri` before storing a device approval cookie.\n */\nexport function validateDeviceInitiateLogin(input: {\n expectedIssuerUrl: string;\n iss: string;\n targetLinkUri: string;\n}): ValidateDeviceInitiateResult {\n const expectedIss = stripTrailingSlashes(input.expectedIssuerUrl.trim());\n let opOrigin: string;\n try {\n opOrigin = new URL(expectedIss).origin;\n } catch {\n return { ok: false, reason: \"server_not_configured\" };\n }\n\n if (normalizeIssuerUrl(input.iss) !== normalizeIssuerUrl(expectedIss)) {\n return { ok: false, reason: \"iss_mismatch\" };\n }\n\n let target: URL;\n try {\n target = new URL(input.targetLinkUri);\n } catch {\n return { ok: false, reason: \"bad_target_uri\" };\n }\n if (target.origin !== opOrigin) {\n return { ok: false, reason: \"target_origin_mismatch\" };\n }\n if (target.pathname !== \"/oidc/device\") {\n return { ok: false, reason: \"target_path_mismatch\" };\n }\n if (target.hash) {\n return { ok: false, reason: \"target_has_hash\" };\n }\n return { ok: true, returnUrl: target.href };\n}\n\n/**\n * Parse PymtHouse `/oidc/device` URL query for `user_code` + `client_id`.\n */\nexport function extractDeviceApprovalFromTargetLink(\n targetHref: string,\n opts?: { expectedIssuerUrl?: string; expectedPublicClientId?: string },\n): DeviceApprovalTuple {\n let target: URL;\n try {\n target = new URL(targetHref);\n } catch {\n return { error: \"bad_target_uri\" };\n }\n\n if (opts?.expectedIssuerUrl) {\n let opOrigin: string;\n try {\n opOrigin = new URL(stripTrailingSlashes(opts.expectedIssuerUrl.trim())).origin;\n } catch {\n return { error: \"target_origin_mismatch\" };\n }\n if (target.origin !== opOrigin) {\n return { error: \"target_origin_mismatch\" };\n }\n }\n\n if (target.pathname !== \"/oidc/device\") {\n return { error: \"target_path_mismatch\" };\n }\n\n const userCodeRaw = target.searchParams.get(\"user_code\")?.trim() ?? \"\";\n const clientIdRaw = target.searchParams.get(\"client_id\")?.trim() ?? \"\";\n if (!userCodeRaw || !USER_CODE_RE.test(userCodeRaw)) {\n return { error: \"invalid_user_code\" };\n }\n if (!clientIdRaw?.startsWith(\"app_\")) {\n return { error: \"invalid_client_id\" };\n }\n if (opts?.expectedPublicClientId && clientIdRaw !== opts.expectedPublicClientId) {\n return { error: \"client_id_mismatch\" };\n }\n return { userCode: userCodeRaw, publicClientId: clientIdRaw };\n}\n"]}
1
+ {"version":3,"sources":["../src/string-utils.ts","../src/device-initiate.ts"],"names":[],"mappings":"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","file":"device-initiate.cjs","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n\nfunction endsWithIgnoreCase(value: string, suffix: string): boolean {\n if (suffix.length > value.length) {\n return false;\n }\n const start = value.length - suffix.length;\n for (let i = 0; i < suffix.length; i++) {\n const a = value.codePointAt(start + i) ?? 0;\n const b = suffix.codePointAt(i) ?? 0;\n if (a !== b && (a | 32) !== (b | 32)) {\n return false;\n }\n }\n return true;\n}\n\nfunction stripSuffixIgnoreCase(value: string, suffix: string): string {\n return endsWithIgnoreCase(value, suffix)\n ? value.slice(0, value.length - suffix.length)\n : value;\n}\n\n/** Issuer URL (`…/oidc`) → Builder API base (`…/api/v1`). Linear-time; no regex. */\nexport function stripOidcPathSuffix(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Issuer URL (`…/api/v1/oidc`) → host origin for signer/API-key routes. Linear-time; no regex. */\nexport function stripIssuerOriginFromOidcUrl(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/api/v1/oidc\");\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Validate gateway session ids before embedding in request URLs. */\nexport function isSafePathSegment(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 128) {\n return false;\n }\n for (let i = 0; i < value.length; i++) {\n const c = value.codePointAt(i) ?? 0;\n const ok =\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45;\n if (!ok) {\n return false;\n }\n }\n return true;\n}\n\n/** Parse and validate an http(s) facade origin (no path). */\nexport function parseHttpOrigin(raw: string | undefined, fallback: string): string {\n const trimmed = (raw ?? fallback).trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new TypeError(\"Origin must be a valid http(s) URL\");\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new TypeError(\"Origin must use http or https\");\n }\n return parsed.origin;\n}\n\n/** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */\nexport function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL {\n if (!isSafePathSegment(sessionId)) {\n throw new TypeError(\"Invalid gateway session id\");\n }\n return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);\n}\n","import { stripTrailingSlashes } from \"./string-utils.js\";\n\n/** RFC 8628 user codes: 4–16 chars, at least one alphanumeric (not all dashes). */\nexport const USER_CODE_RE = /^(?=.*[A-Z0-9])[A-Z0-9-]{4,16}$/;\n\nexport type ValidateDeviceInitiateResult =\n | { ok: true; returnUrl: string }\n | { ok: false; reason: string };\n\nexport type DeviceApprovalTuple =\n | { userCode: string; publicClientId: string }\n | { error: string };\n\nfunction normalizeIssuerUrl(iss: string): string {\n try {\n return stripTrailingSlashes(new URL(iss.trim()).href);\n } catch {\n return iss.trim();\n }\n}\n\n/**\n * Validate OP-issued `iss` + `target_link_uri` before storing a device approval cookie.\n */\nexport function validateDeviceInitiateLogin(input: {\n expectedIssuerUrl: string;\n iss: string;\n targetLinkUri: string;\n}): ValidateDeviceInitiateResult {\n const expectedIss = stripTrailingSlashes(input.expectedIssuerUrl.trim());\n let opOrigin: string;\n try {\n opOrigin = new URL(expectedIss).origin;\n } catch {\n return { ok: false, reason: \"server_not_configured\" };\n }\n\n if (normalizeIssuerUrl(input.iss) !== normalizeIssuerUrl(expectedIss)) {\n return { ok: false, reason: \"iss_mismatch\" };\n }\n\n let target: URL;\n try {\n target = new URL(input.targetLinkUri);\n } catch {\n return { ok: false, reason: \"bad_target_uri\" };\n }\n if (target.origin !== opOrigin) {\n return { ok: false, reason: \"target_origin_mismatch\" };\n }\n if (target.pathname !== \"/oidc/device\") {\n return { ok: false, reason: \"target_path_mismatch\" };\n }\n if (target.hash) {\n return { ok: false, reason: \"target_has_hash\" };\n }\n return { ok: true, returnUrl: target.href };\n}\n\n/**\n * Parse PymtHouse `/oidc/device` URL query for `user_code` + `client_id`.\n */\nexport function extractDeviceApprovalFromTargetLink(\n targetHref: string,\n opts?: { expectedIssuerUrl?: string; expectedPublicClientId?: string },\n): DeviceApprovalTuple {\n let target: URL;\n try {\n target = new URL(targetHref);\n } catch {\n return { error: \"bad_target_uri\" };\n }\n\n if (opts?.expectedIssuerUrl) {\n let opOrigin: string;\n try {\n opOrigin = new URL(stripTrailingSlashes(opts.expectedIssuerUrl.trim())).origin;\n } catch {\n return { error: \"target_origin_mismatch\" };\n }\n if (target.origin !== opOrigin) {\n return { error: \"target_origin_mismatch\" };\n }\n }\n\n if (target.pathname !== \"/oidc/device\") {\n return { error: \"target_path_mismatch\" };\n }\n\n const userCodeRaw = target.searchParams.get(\"user_code\")?.trim() ?? \"\";\n const clientIdRaw = target.searchParams.get(\"client_id\")?.trim() ?? \"\";\n if (!userCodeRaw || !USER_CODE_RE.test(userCodeRaw)) {\n return { error: \"invalid_user_code\" };\n }\n if (!clientIdRaw?.startsWith(\"app_\")) {\n return { error: \"invalid_client_id\" };\n }\n if (opts?.expectedPublicClientId && clientIdRaw !== opts.expectedPublicClientId) {\n return { error: \"client_id_mismatch\" };\n }\n return { userCode: userCodeRaw, publicClientId: clientIdRaw };\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  // src/string-utils.ts
2
2
  function stripTrailingSlashes(value) {
3
3
  let end = value.length;
4
- while (end > 0 && value.charCodeAt(end - 1) === 47) {
4
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
5
5
  end--;
6
6
  }
7
7
  return value.slice(0, end);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/string-utils.ts","../src/device-initiate.ts"],"names":[],"mappings":"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","file":"device-initiate.js","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n","import { stripTrailingSlashes } from \"./string-utils.js\";\n\n/** RFC 8628 user codes: 4–16 chars, at least one alphanumeric (not all dashes). */\nexport const USER_CODE_RE = /^(?=.*[A-Z0-9])[A-Z0-9-]{4,16}$/;\n\nexport type ValidateDeviceInitiateResult =\n | { ok: true; returnUrl: string }\n | { ok: false; reason: string };\n\nexport type DeviceApprovalTuple =\n | { userCode: string; publicClientId: string }\n | { error: string };\n\nfunction normalizeIssuerUrl(iss: string): string {\n try {\n return stripTrailingSlashes(new URL(iss.trim()).href);\n } catch {\n return iss.trim();\n }\n}\n\n/**\n * Validate OP-issued `iss` + `target_link_uri` before storing a device approval cookie.\n */\nexport function validateDeviceInitiateLogin(input: {\n expectedIssuerUrl: string;\n iss: string;\n targetLinkUri: string;\n}): ValidateDeviceInitiateResult {\n const expectedIss = stripTrailingSlashes(input.expectedIssuerUrl.trim());\n let opOrigin: string;\n try {\n opOrigin = new URL(expectedIss).origin;\n } catch {\n return { ok: false, reason: \"server_not_configured\" };\n }\n\n if (normalizeIssuerUrl(input.iss) !== normalizeIssuerUrl(expectedIss)) {\n return { ok: false, reason: \"iss_mismatch\" };\n }\n\n let target: URL;\n try {\n target = new URL(input.targetLinkUri);\n } catch {\n return { ok: false, reason: \"bad_target_uri\" };\n }\n if (target.origin !== opOrigin) {\n return { ok: false, reason: \"target_origin_mismatch\" };\n }\n if (target.pathname !== \"/oidc/device\") {\n return { ok: false, reason: \"target_path_mismatch\" };\n }\n if (target.hash) {\n return { ok: false, reason: \"target_has_hash\" };\n }\n return { ok: true, returnUrl: target.href };\n}\n\n/**\n * Parse PymtHouse `/oidc/device` URL query for `user_code` + `client_id`.\n */\nexport function extractDeviceApprovalFromTargetLink(\n targetHref: string,\n opts?: { expectedIssuerUrl?: string; expectedPublicClientId?: string },\n): DeviceApprovalTuple {\n let target: URL;\n try {\n target = new URL(targetHref);\n } catch {\n return { error: \"bad_target_uri\" };\n }\n\n if (opts?.expectedIssuerUrl) {\n let opOrigin: string;\n try {\n opOrigin = new URL(stripTrailingSlashes(opts.expectedIssuerUrl.trim())).origin;\n } catch {\n return { error: \"target_origin_mismatch\" };\n }\n if (target.origin !== opOrigin) {\n return { error: \"target_origin_mismatch\" };\n }\n }\n\n if (target.pathname !== \"/oidc/device\") {\n return { error: \"target_path_mismatch\" };\n }\n\n const userCodeRaw = target.searchParams.get(\"user_code\")?.trim() ?? \"\";\n const clientIdRaw = target.searchParams.get(\"client_id\")?.trim() ?? \"\";\n if (!userCodeRaw || !USER_CODE_RE.test(userCodeRaw)) {\n return { error: \"invalid_user_code\" };\n }\n if (!clientIdRaw?.startsWith(\"app_\")) {\n return { error: \"invalid_client_id\" };\n }\n if (opts?.expectedPublicClientId && clientIdRaw !== opts.expectedPublicClientId) {\n return { error: \"client_id_mismatch\" };\n }\n return { userCode: userCodeRaw, publicClientId: clientIdRaw };\n}\n"]}
1
+ {"version":3,"sources":["../src/string-utils.ts","../src/device-initiate.ts"],"names":[],"mappings":"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","file":"device-initiate.js","sourcesContent":["/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n\nfunction endsWithIgnoreCase(value: string, suffix: string): boolean {\n if (suffix.length > value.length) {\n return false;\n }\n const start = value.length - suffix.length;\n for (let i = 0; i < suffix.length; i++) {\n const a = value.codePointAt(start + i) ?? 0;\n const b = suffix.codePointAt(i) ?? 0;\n if (a !== b && (a | 32) !== (b | 32)) {\n return false;\n }\n }\n return true;\n}\n\nfunction stripSuffixIgnoreCase(value: string, suffix: string): string {\n return endsWithIgnoreCase(value, suffix)\n ? value.slice(0, value.length - suffix.length)\n : value;\n}\n\n/** Issuer URL (`…/oidc`) → Builder API base (`…/api/v1`). Linear-time; no regex. */\nexport function stripOidcPathSuffix(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Issuer URL (`…/api/v1/oidc`) → host origin for signer/API-key routes. Linear-time; no regex. */\nexport function stripIssuerOriginFromOidcUrl(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/api/v1/oidc\");\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Validate gateway session ids before embedding in request URLs. */\nexport function isSafePathSegment(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length === 0 || value.length > 128) {\n return false;\n }\n for (let i = 0; i < value.length; i++) {\n const c = value.codePointAt(i) ?? 0;\n const ok =\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45;\n if (!ok) {\n return false;\n }\n }\n return true;\n}\n\n/** Parse and validate an http(s) facade origin (no path). */\nexport function parseHttpOrigin(raw: string | undefined, fallback: string): string {\n const trimmed = (raw ?? fallback).trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new TypeError(\"Origin must be a valid http(s) URL\");\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new TypeError(\"Origin must use http or https\");\n }\n return parsed.origin;\n}\n\n/** Build a validated DELETE URL for `/api/gateway/sessions/:id`. */\nexport function buildGatewaySessionDeleteUrl(origin: string, sessionId: string): URL {\n if (!isSafePathSegment(sessionId)) {\n throw new TypeError(\"Invalid gateway session id\");\n }\n return new URL(`/api/gateway/sessions/${encodeURIComponent(sessionId)}`, origin);\n}\n","import { stripTrailingSlashes } from \"./string-utils.js\";\n\n/** RFC 8628 user codes: 4–16 chars, at least one alphanumeric (not all dashes). */\nexport const USER_CODE_RE = /^(?=.*[A-Z0-9])[A-Z0-9-]{4,16}$/;\n\nexport type ValidateDeviceInitiateResult =\n | { ok: true; returnUrl: string }\n | { ok: false; reason: string };\n\nexport type DeviceApprovalTuple =\n | { userCode: string; publicClientId: string }\n | { error: string };\n\nfunction normalizeIssuerUrl(iss: string): string {\n try {\n return stripTrailingSlashes(new URL(iss.trim()).href);\n } catch {\n return iss.trim();\n }\n}\n\n/**\n * Validate OP-issued `iss` + `target_link_uri` before storing a device approval cookie.\n */\nexport function validateDeviceInitiateLogin(input: {\n expectedIssuerUrl: string;\n iss: string;\n targetLinkUri: string;\n}): ValidateDeviceInitiateResult {\n const expectedIss = stripTrailingSlashes(input.expectedIssuerUrl.trim());\n let opOrigin: string;\n try {\n opOrigin = new URL(expectedIss).origin;\n } catch {\n return { ok: false, reason: \"server_not_configured\" };\n }\n\n if (normalizeIssuerUrl(input.iss) !== normalizeIssuerUrl(expectedIss)) {\n return { ok: false, reason: \"iss_mismatch\" };\n }\n\n let target: URL;\n try {\n target = new URL(input.targetLinkUri);\n } catch {\n return { ok: false, reason: \"bad_target_uri\" };\n }\n if (target.origin !== opOrigin) {\n return { ok: false, reason: \"target_origin_mismatch\" };\n }\n if (target.pathname !== \"/oidc/device\") {\n return { ok: false, reason: \"target_path_mismatch\" };\n }\n if (target.hash) {\n return { ok: false, reason: \"target_has_hash\" };\n }\n return { ok: true, returnUrl: target.href };\n}\n\n/**\n * Parse PymtHouse `/oidc/device` URL query for `user_code` + `client_id`.\n */\nexport function extractDeviceApprovalFromTargetLink(\n targetHref: string,\n opts?: { expectedIssuerUrl?: string; expectedPublicClientId?: string },\n): DeviceApprovalTuple {\n let target: URL;\n try {\n target = new URL(targetHref);\n } catch {\n return { error: \"bad_target_uri\" };\n }\n\n if (opts?.expectedIssuerUrl) {\n let opOrigin: string;\n try {\n opOrigin = new URL(stripTrailingSlashes(opts.expectedIssuerUrl.trim())).origin;\n } catch {\n return { error: \"target_origin_mismatch\" };\n }\n if (target.origin !== opOrigin) {\n return { error: \"target_origin_mismatch\" };\n }\n }\n\n if (target.pathname !== \"/oidc/device\") {\n return { error: \"target_path_mismatch\" };\n }\n\n const userCodeRaw = target.searchParams.get(\"user_code\")?.trim() ?? \"\";\n const clientIdRaw = target.searchParams.get(\"client_id\")?.trim() ?? \"\";\n if (!userCodeRaw || !USER_CODE_RE.test(userCodeRaw)) {\n return { error: \"invalid_user_code\" };\n }\n if (!clientIdRaw?.startsWith(\"app_\")) {\n return { error: \"invalid_client_id\" };\n }\n if (opts?.expectedPublicClientId && clientIdRaw !== opts.expectedPublicClientId) {\n return { error: \"client_id_mismatch\" };\n }\n return { userCode: userCodeRaw, publicClientId: clientIdRaw };\n}\n"]}
package/dist/device.cjs CHANGED
@@ -25,7 +25,7 @@ var PmtHouseError = class extends Error {
25
25
  // src/string-utils.ts
26
26
  function stripTrailingSlashes(value) {
27
27
  let end = value.length;
28
- while (end > 0 && value.charCodeAt(end - 1) === 47) {
28
+ while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
29
29
  end--;
30
30
  }
31
31
  return value.slice(0, end);