@pymthouse/builder-sdk 0.0.8 → 0.1.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.
package/README.md CHANGED
@@ -66,14 +66,52 @@ const signerSession = await client.mintUserSignerSessionToken({
66
66
  For advanced flows that already have a user JWT, call
67
67
  `exchangeForSignerSession({ userJwt })` directly.
68
68
 
69
+ Integrators can use the higher-level workflow helpers:
70
+
71
+ ```ts
72
+ const session = await client.mintSignerSessionForExternalUser({
73
+ externalUserId: "naap-user-123",
74
+ email: "user@example.com",
75
+ });
76
+ // session.accessToken is opaque pmth_…
77
+
78
+ await client.approveDeviceLogin({
79
+ externalUserId: "naap-user-123",
80
+ userCode: "ABCD-EFGH",
81
+ publicClientId: process.env.PYMTHOUSE_PUBLIC_CLIENT_ID,
82
+ });
83
+ ```
84
+
85
+ ## Usage API: session-scoped `scope=me` BFF helper
86
+
87
+ ```ts
88
+ const payload = await client.fetchUsageForExternalUser({
89
+ externalUserId: "naap-user-123",
90
+ startDate,
91
+ endDate,
92
+ });
93
+ // payload.currentUser includes fiat totals + merged pipelineModels
94
+ ```
95
+
96
+ ## App manifest
97
+
98
+ ```ts
99
+ const { manifest, etag, notModified } = await client.getAppManifest({
100
+ ifNoneMatch: cachedEtag ?? undefined,
101
+ });
102
+ ```
103
+
69
104
  ## Subpath exports
70
105
 
71
106
  | Import | Purpose |
72
107
  |--------|---------|
73
- | `@pymthouse/builder-sdk` | `PmtHouseClient`, discovery cache, errors, usage aggregation helpers |
108
+ | `@pymthouse/builder-sdk` | `PmtHouseClient`, usage helpers, manifest parsers, token helpers |
109
+ | `@pymthouse/builder-sdk/config` | `isPymthouseConfigured`, `readPymthouseEnv` (Edge/middleware-safe) |
110
+ | `@pymthouse/builder-sdk/tokens` | Signer session TTL, JWT shape helpers, `parseSignerSessionExchange` |
74
111
  | `@pymthouse/builder-sdk/format` | Wei formatting for Usage API |
75
- | `@pymthouse/builder-sdk/env` | `createPmtHouseClientFromEnv`, `getPymthouseBaseUrl` |
112
+ | `@pymthouse/builder-sdk/env` | `createPmtHouseClientFromEnv`, `getPymthouseBaseUrl` (server-only) |
76
113
  | `@pymthouse/builder-sdk/device` | RFC 8628 `pollDeviceToken` |
114
+ | `@pymthouse/builder-sdk/device-initiate` | Option B device login validation (Edge-safe) |
77
115
  | `@pymthouse/builder-sdk/verify` | RFC 9068 `verifyJwt` |
78
116
 
79
117
  ## Usage API: duplicate `byUser` rows
@@ -1,4 +1,5 @@
1
- import { f as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, P as ParsedDeviceApprovalRedirect, A as AppUserRecord, g as UpsertAppUserInput, M as MintUserAccessTokenInput, d as MintUserAccessTokenResponse, D as DeviceApprovalInput, T as TokenExchangeResponse, C as ClientCredentialsTokenResponse, e as MintUserSignerSessionTokenInput, h as UsageQueryInput, b as UsageApiResponse } from './types-W9PJAspR.cjs';
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
3
 
3
4
  /**
4
5
  * Normalize RFC 8628 user codes for comparison and resource URIs (uppercase, strip separators).
@@ -45,24 +46,36 @@ declare class PmtHouseClient {
45
46
  userJwt?: string;
46
47
  }): Promise<TokenExchangeResponse>;
47
48
  getUsage(input?: UsageQueryInput): Promise<UsageApiResponse>;
49
+ /**
50
+ * Session-scoped usage for one `externalUserId`: user rollup plus merged pipeline/model breakdown.
51
+ */
52
+ fetchUsageForExternalUser(input: {
53
+ externalUserId: string;
54
+ startDate: string;
55
+ endDate: string;
56
+ maxEndUserIds?: number;
57
+ }): Promise<MeScopeUsagePayload>;
58
+ getAppManifest(opts?: {
59
+ ifNoneMatch?: string;
60
+ signal?: AbortSignal;
61
+ }): Promise<GetAppManifestResult>;
62
+ /**
63
+ * Upsert an external user, mint a short-lived JWT, and exchange for an opaque signer session.
64
+ */
65
+ mintSignerSessionForExternalUser(input: MintSignerSessionForExternalUserInput): Promise<SignerSessionToken>;
66
+ /**
67
+ * Approve a pending RFC 8628 device code for an external user (Option B).
68
+ */
69
+ approveDeviceLogin(input: ApproveDeviceLoginInput): Promise<void>;
48
70
  private tokenEndpointFetchOptions;
49
71
  private getAppsBaseUrl;
50
72
  private getIssuerOrigin;
51
73
  private builderHeaders;
74
+ private builderHeadersRecord;
52
75
  private m2mClientAuth;
53
76
  private requestJson;
54
77
  private safeParseJson;
55
78
  private asError;
56
79
  }
57
80
 
58
- /**
59
- * Site origin for the PymtHouse deployment (e.g. https://pymthouse.com), derived
60
- * from `PYMTHOUSE_ISSUER_URL`.
61
- */
62
- declare function getPymthouseBaseUrl(): string;
63
- /**
64
- * Singleton `PmtHouseClient` from `PYMTHOUSE_*` environment variables (server-side).
65
- */
66
- declare function createPmtHouseClientFromEnv(): PmtHouseClient;
67
-
68
- export { PmtHouseClient as P, buildDeviceCodeResource as b, createPmtHouseClientFromEnv as c, getPymthouseBaseUrl as g, normalizeUserCode as n };
81
+ export { PmtHouseClient as P, buildDeviceCodeResource as b, normalizeUserCode as n };
@@ -1,4 +1,5 @@
1
- import { f as PmtHouseClientOptions, G as GetDiscoveryOptions, O as OidcDiscoveryDocument, P as ParsedDeviceApprovalRedirect, A as AppUserRecord, g as UpsertAppUserInput, M as MintUserAccessTokenInput, d as MintUserAccessTokenResponse, D as DeviceApprovalInput, T as TokenExchangeResponse, C as ClientCredentialsTokenResponse, e as MintUserSignerSessionTokenInput, h as UsageQueryInput, b as UsageApiResponse } from './types-W9PJAspR.js';
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
3
 
3
4
  /**
4
5
  * Normalize RFC 8628 user codes for comparison and resource URIs (uppercase, strip separators).
@@ -45,24 +46,36 @@ declare class PmtHouseClient {
45
46
  userJwt?: string;
46
47
  }): Promise<TokenExchangeResponse>;
47
48
  getUsage(input?: UsageQueryInput): Promise<UsageApiResponse>;
49
+ /**
50
+ * Session-scoped usage for one `externalUserId`: user rollup plus merged pipeline/model breakdown.
51
+ */
52
+ fetchUsageForExternalUser(input: {
53
+ externalUserId: string;
54
+ startDate: string;
55
+ endDate: string;
56
+ maxEndUserIds?: number;
57
+ }): Promise<MeScopeUsagePayload>;
58
+ getAppManifest(opts?: {
59
+ ifNoneMatch?: string;
60
+ signal?: AbortSignal;
61
+ }): Promise<GetAppManifestResult>;
62
+ /**
63
+ * Upsert an external user, mint a short-lived JWT, and exchange for an opaque signer session.
64
+ */
65
+ mintSignerSessionForExternalUser(input: MintSignerSessionForExternalUserInput): Promise<SignerSessionToken>;
66
+ /**
67
+ * Approve a pending RFC 8628 device code for an external user (Option B).
68
+ */
69
+ approveDeviceLogin(input: ApproveDeviceLoginInput): Promise<void>;
48
70
  private tokenEndpointFetchOptions;
49
71
  private getAppsBaseUrl;
50
72
  private getIssuerOrigin;
51
73
  private builderHeaders;
74
+ private builderHeadersRecord;
52
75
  private m2mClientAuth;
53
76
  private requestJson;
54
77
  private safeParseJson;
55
78
  private asError;
56
79
  }
57
80
 
58
- /**
59
- * Site origin for the PymtHouse deployment (e.g. https://pymthouse.com), derived
60
- * from `PYMTHOUSE_ISSUER_URL`.
61
- */
62
- declare function getPymthouseBaseUrl(): string;
63
- /**
64
- * Singleton `PmtHouseClient` from `PYMTHOUSE_*` environment variables (server-side).
65
- */
66
- declare function createPmtHouseClientFromEnv(): PmtHouseClient;
67
-
68
- export { PmtHouseClient as P, buildDeviceCodeResource as b, createPmtHouseClientFromEnv as c, getPymthouseBaseUrl as g, normalizeUserCode as n };
81
+ export { PmtHouseClient as P, buildDeviceCodeResource as b, normalizeUserCode as n };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ // src/string-utils.ts
4
+ function stripTrailingSlashes(value) {
5
+ let end = value.length;
6
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
7
+ end--;
8
+ }
9
+ return value.slice(0, end);
10
+ }
11
+
12
+ // src/config.ts
13
+ 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.";
14
+ function trimEnv(name) {
15
+ const value = process.env[name];
16
+ if (!value) return null;
17
+ const trimmed = value.trim();
18
+ return trimmed || null;
19
+ }
20
+ function readPymthouseEnv() {
21
+ const issuerUrl = trimEnv("PYMTHOUSE_ISSUER_URL");
22
+ const publicClientId = trimEnv("PYMTHOUSE_PUBLIC_CLIENT_ID");
23
+ const m2mClientId = trimEnv("PYMTHOUSE_M2M_CLIENT_ID");
24
+ const m2mClientSecret = trimEnv("PYMTHOUSE_M2M_CLIENT_SECRET");
25
+ if (!issuerUrl || !publicClientId || !m2mClientId || !m2mClientSecret) {
26
+ return null;
27
+ }
28
+ return {
29
+ issuerUrl: stripTrailingSlashes(issuerUrl),
30
+ publicClientId,
31
+ m2mClientId,
32
+ m2mClientSecret
33
+ };
34
+ }
35
+ function getPymthouseIssuerUrlFromEnv() {
36
+ const raw = trimEnv("PYMTHOUSE_ISSUER_URL");
37
+ if (!raw) return null;
38
+ try {
39
+ return stripTrailingSlashes(new URL(raw).href);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function getPymthousePublicClientIdFromEnv() {
45
+ return trimEnv("PYMTHOUSE_PUBLIC_CLIENT_ID");
46
+ }
47
+ function isPymthouseConfigured() {
48
+ return readPymthouseEnv() !== null;
49
+ }
50
+ function getBuilderApiV1BaseFromIssuerUrl(issuerUrl) {
51
+ const noTrail = stripTrailingSlashes(issuerUrl.trim());
52
+ return noTrail.replace(/\/oidc\/?$/i, "");
53
+ }
54
+ function getPymthouseIssuerOrigin(issuerUrl) {
55
+ return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;
56
+ }
57
+
58
+ exports.PYMTHOUSE_NOT_CONFIGURED_MESSAGE = PYMTHOUSE_NOT_CONFIGURED_MESSAGE;
59
+ exports.getBuilderApiV1BaseFromIssuerUrl = getBuilderApiV1BaseFromIssuerUrl;
60
+ exports.getPymthouseIssuerOrigin = getPymthouseIssuerOrigin;
61
+ exports.getPymthouseIssuerUrlFromEnv = getPymthouseIssuerUrlFromEnv;
62
+ exports.getPymthousePublicClientIdFromEnv = getPymthousePublicClientIdFromEnv;
63
+ exports.isPymthouseConfigured = isPymthouseConfigured;
64
+ exports.readPymthouseEnv = readPymthouseEnv;
65
+ //# sourceMappingURL=config.cjs.map
66
+ //# sourceMappingURL=config.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,22 @@
1
+ /** Operator hint when Builder / Usage cannot run. */
2
+ 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
+ interface PymthouseEnvConfig {
4
+ issuerUrl: string;
5
+ publicClientId: string;
6
+ m2mClientId: string;
7
+ m2mClientSecret: string;
8
+ }
9
+ /** Read `PYMTHOUSE_*` env vars without throwing. Returns null when incomplete. */
10
+ declare function readPymthouseEnv(): PymthouseEnvConfig | null;
11
+ /** Read `PYMTHOUSE_ISSUER_URL` without requiring full M2M configuration. */
12
+ declare function getPymthouseIssuerUrlFromEnv(): string | null;
13
+ /** Read `PYMTHOUSE_PUBLIC_CLIENT_ID` without requiring full M2M configuration. */
14
+ declare function getPymthousePublicClientIdFromEnv(): string | null;
15
+ /** True when all vars required by `createPmtHouseClientFromEnv` are present. */
16
+ declare function isPymthouseConfigured(): boolean;
17
+ /** Resolve Builder API base (`…/api/v1`) from issuer URL (`…/api/v1/oidc`). */
18
+ declare function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string;
19
+ /** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */
20
+ declare function getPymthouseIssuerOrigin(issuerUrl: string): string;
21
+
22
+ export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, type PymthouseEnvConfig, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, readPymthouseEnv };
@@ -0,0 +1,22 @@
1
+ /** Operator hint when Builder / Usage cannot run. */
2
+ 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
+ interface PymthouseEnvConfig {
4
+ issuerUrl: string;
5
+ publicClientId: string;
6
+ m2mClientId: string;
7
+ m2mClientSecret: string;
8
+ }
9
+ /** Read `PYMTHOUSE_*` env vars without throwing. Returns null when incomplete. */
10
+ declare function readPymthouseEnv(): PymthouseEnvConfig | null;
11
+ /** Read `PYMTHOUSE_ISSUER_URL` without requiring full M2M configuration. */
12
+ declare function getPymthouseIssuerUrlFromEnv(): string | null;
13
+ /** Read `PYMTHOUSE_PUBLIC_CLIENT_ID` without requiring full M2M configuration. */
14
+ declare function getPymthousePublicClientIdFromEnv(): string | null;
15
+ /** True when all vars required by `createPmtHouseClientFromEnv` are present. */
16
+ declare function isPymthouseConfigured(): boolean;
17
+ /** Resolve Builder API base (`…/api/v1`) from issuer URL (`…/api/v1/oidc`). */
18
+ declare function getBuilderApiV1BaseFromIssuerUrl(issuerUrl: string): string;
19
+ /** Origin of the OIDC issuer host (e.g. `https://pymthouse.com`). */
20
+ declare function getPymthouseIssuerOrigin(issuerUrl: string): string;
21
+
22
+ export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, type PymthouseEnvConfig, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, readPymthouseEnv };
package/dist/config.js ADDED
@@ -0,0 +1,58 @@
1
+ // src/string-utils.ts
2
+ function stripTrailingSlashes(value) {
3
+ let end = value.length;
4
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
5
+ end--;
6
+ }
7
+ return value.slice(0, end);
8
+ }
9
+
10
+ // src/config.ts
11
+ 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.";
12
+ function trimEnv(name) {
13
+ const value = process.env[name];
14
+ if (!value) return null;
15
+ const trimmed = value.trim();
16
+ return trimmed || null;
17
+ }
18
+ function readPymthouseEnv() {
19
+ const issuerUrl = trimEnv("PYMTHOUSE_ISSUER_URL");
20
+ const publicClientId = trimEnv("PYMTHOUSE_PUBLIC_CLIENT_ID");
21
+ const m2mClientId = trimEnv("PYMTHOUSE_M2M_CLIENT_ID");
22
+ const m2mClientSecret = trimEnv("PYMTHOUSE_M2M_CLIENT_SECRET");
23
+ if (!issuerUrl || !publicClientId || !m2mClientId || !m2mClientSecret) {
24
+ return null;
25
+ }
26
+ return {
27
+ issuerUrl: stripTrailingSlashes(issuerUrl),
28
+ publicClientId,
29
+ m2mClientId,
30
+ m2mClientSecret
31
+ };
32
+ }
33
+ function getPymthouseIssuerUrlFromEnv() {
34
+ const raw = trimEnv("PYMTHOUSE_ISSUER_URL");
35
+ if (!raw) return null;
36
+ try {
37
+ return stripTrailingSlashes(new URL(raw).href);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ function getPymthousePublicClientIdFromEnv() {
43
+ return trimEnv("PYMTHOUSE_PUBLIC_CLIENT_ID");
44
+ }
45
+ function isPymthouseConfigured() {
46
+ return readPymthouseEnv() !== null;
47
+ }
48
+ function getBuilderApiV1BaseFromIssuerUrl(issuerUrl) {
49
+ const noTrail = stripTrailingSlashes(issuerUrl.trim());
50
+ return noTrail.replace(/\/oidc\/?$/i, "");
51
+ }
52
+ function getPymthouseIssuerOrigin(issuerUrl) {
53
+ return new URL(stripTrailingSlashes(issuerUrl.trim())).origin;
54
+ }
55
+
56
+ export { PYMTHOUSE_NOT_CONFIGURED_MESSAGE, getBuilderApiV1BaseFromIssuerUrl, getPymthouseIssuerOrigin, getPymthouseIssuerUrlFromEnv, getPymthousePublicClientIdFromEnv, isPymthouseConfigured, readPymthouseEnv };
57
+ //# sourceMappingURL=config.js.map
58
+ //# sourceMappingURL=config.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // src/string-utils.ts
4
+ function stripTrailingSlashes(value) {
5
+ let end = value.length;
6
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
7
+ end--;
8
+ }
9
+ return value.slice(0, end);
10
+ }
11
+
12
+ // src/device-initiate.ts
13
+ var USER_CODE_RE = /^(?=.*[A-Z0-9])[A-Z0-9-]{4,16}$/;
14
+ function normalizeIssuerUrl(iss) {
15
+ try {
16
+ return stripTrailingSlashes(new URL(iss.trim()).href);
17
+ } catch {
18
+ return iss.trim();
19
+ }
20
+ }
21
+ function validateDeviceInitiateLogin(input) {
22
+ const expectedIss = stripTrailingSlashes(input.expectedIssuerUrl.trim());
23
+ let opOrigin;
24
+ try {
25
+ opOrigin = new URL(expectedIss).origin;
26
+ } catch {
27
+ return { ok: false, reason: "server_not_configured" };
28
+ }
29
+ if (normalizeIssuerUrl(input.iss) !== normalizeIssuerUrl(expectedIss)) {
30
+ return { ok: false, reason: "iss_mismatch" };
31
+ }
32
+ let target;
33
+ try {
34
+ target = new URL(input.targetLinkUri);
35
+ } catch {
36
+ return { ok: false, reason: "bad_target_uri" };
37
+ }
38
+ if (target.origin !== opOrigin) {
39
+ return { ok: false, reason: "target_origin_mismatch" };
40
+ }
41
+ if (target.pathname !== "/oidc/device") {
42
+ return { ok: false, reason: "target_path_mismatch" };
43
+ }
44
+ if (target.hash) {
45
+ return { ok: false, reason: "target_has_hash" };
46
+ }
47
+ return { ok: true, returnUrl: target.href };
48
+ }
49
+ function extractDeviceApprovalFromTargetLink(targetHref, opts) {
50
+ let target;
51
+ try {
52
+ target = new URL(targetHref);
53
+ } catch {
54
+ return { error: "bad_target_uri" };
55
+ }
56
+ if (opts?.expectedIssuerUrl) {
57
+ let opOrigin;
58
+ try {
59
+ opOrigin = new URL(stripTrailingSlashes(opts.expectedIssuerUrl.trim())).origin;
60
+ } catch {
61
+ return { error: "target_origin_mismatch" };
62
+ }
63
+ if (target.origin !== opOrigin) {
64
+ return { error: "target_origin_mismatch" };
65
+ }
66
+ }
67
+ if (target.pathname !== "/oidc/device") {
68
+ return { error: "target_path_mismatch" };
69
+ }
70
+ const userCodeRaw = target.searchParams.get("user_code")?.trim() ?? "";
71
+ const clientIdRaw = target.searchParams.get("client_id")?.trim() ?? "";
72
+ if (!userCodeRaw || !USER_CODE_RE.test(userCodeRaw)) {
73
+ return { error: "invalid_user_code" };
74
+ }
75
+ if (!clientIdRaw?.startsWith("app_")) {
76
+ return { error: "invalid_client_id" };
77
+ }
78
+ if (opts?.expectedPublicClientId && clientIdRaw !== opts.expectedPublicClientId) {
79
+ return { error: "client_id_mismatch" };
80
+ }
81
+ return { userCode: userCodeRaw, publicClientId: clientIdRaw };
82
+ }
83
+
84
+ exports.USER_CODE_RE = USER_CODE_RE;
85
+ exports.extractDeviceApprovalFromTargetLink = extractDeviceApprovalFromTargetLink;
86
+ exports.validateDeviceInitiateLogin = validateDeviceInitiateLogin;
87
+ //# sourceMappingURL=device-initiate.cjs.map
88
+ //# sourceMappingURL=device-initiate.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,32 @@
1
+ /** RFC 8628 user codes: 4–16 chars, at least one alphanumeric (not all dashes). */
2
+ declare const USER_CODE_RE: RegExp;
3
+ type ValidateDeviceInitiateResult = {
4
+ ok: true;
5
+ returnUrl: string;
6
+ } | {
7
+ ok: false;
8
+ reason: string;
9
+ };
10
+ type DeviceApprovalTuple = {
11
+ userCode: string;
12
+ publicClientId: string;
13
+ } | {
14
+ error: string;
15
+ };
16
+ /**
17
+ * Validate OP-issued `iss` + `target_link_uri` before storing a device approval cookie.
18
+ */
19
+ declare function validateDeviceInitiateLogin(input: {
20
+ expectedIssuerUrl: string;
21
+ iss: string;
22
+ targetLinkUri: string;
23
+ }): ValidateDeviceInitiateResult;
24
+ /**
25
+ * Parse PymtHouse `/oidc/device` URL query for `user_code` + `client_id`.
26
+ */
27
+ declare function extractDeviceApprovalFromTargetLink(targetHref: string, opts?: {
28
+ expectedIssuerUrl?: string;
29
+ expectedPublicClientId?: string;
30
+ }): DeviceApprovalTuple;
31
+
32
+ export { type DeviceApprovalTuple, USER_CODE_RE, type ValidateDeviceInitiateResult, extractDeviceApprovalFromTargetLink, validateDeviceInitiateLogin };
@@ -0,0 +1,32 @@
1
+ /** RFC 8628 user codes: 4–16 chars, at least one alphanumeric (not all dashes). */
2
+ declare const USER_CODE_RE: RegExp;
3
+ type ValidateDeviceInitiateResult = {
4
+ ok: true;
5
+ returnUrl: string;
6
+ } | {
7
+ ok: false;
8
+ reason: string;
9
+ };
10
+ type DeviceApprovalTuple = {
11
+ userCode: string;
12
+ publicClientId: string;
13
+ } | {
14
+ error: string;
15
+ };
16
+ /**
17
+ * Validate OP-issued `iss` + `target_link_uri` before storing a device approval cookie.
18
+ */
19
+ declare function validateDeviceInitiateLogin(input: {
20
+ expectedIssuerUrl: string;
21
+ iss: string;
22
+ targetLinkUri: string;
23
+ }): ValidateDeviceInitiateResult;
24
+ /**
25
+ * Parse PymtHouse `/oidc/device` URL query for `user_code` + `client_id`.
26
+ */
27
+ declare function extractDeviceApprovalFromTargetLink(targetHref: string, opts?: {
28
+ expectedIssuerUrl?: string;
29
+ expectedPublicClientId?: string;
30
+ }): DeviceApprovalTuple;
31
+
32
+ export { type DeviceApprovalTuple, USER_CODE_RE, type ValidateDeviceInitiateResult, extractDeviceApprovalFromTargetLink, validateDeviceInitiateLogin };
@@ -0,0 +1,84 @@
1
+ // src/string-utils.ts
2
+ function stripTrailingSlashes(value) {
3
+ let end = value.length;
4
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
5
+ end--;
6
+ }
7
+ return value.slice(0, end);
8
+ }
9
+
10
+ // src/device-initiate.ts
11
+ var USER_CODE_RE = /^(?=.*[A-Z0-9])[A-Z0-9-]{4,16}$/;
12
+ function normalizeIssuerUrl(iss) {
13
+ try {
14
+ return stripTrailingSlashes(new URL(iss.trim()).href);
15
+ } catch {
16
+ return iss.trim();
17
+ }
18
+ }
19
+ function validateDeviceInitiateLogin(input) {
20
+ const expectedIss = stripTrailingSlashes(input.expectedIssuerUrl.trim());
21
+ let opOrigin;
22
+ try {
23
+ opOrigin = new URL(expectedIss).origin;
24
+ } catch {
25
+ return { ok: false, reason: "server_not_configured" };
26
+ }
27
+ if (normalizeIssuerUrl(input.iss) !== normalizeIssuerUrl(expectedIss)) {
28
+ return { ok: false, reason: "iss_mismatch" };
29
+ }
30
+ let target;
31
+ try {
32
+ target = new URL(input.targetLinkUri);
33
+ } catch {
34
+ return { ok: false, reason: "bad_target_uri" };
35
+ }
36
+ if (target.origin !== opOrigin) {
37
+ return { ok: false, reason: "target_origin_mismatch" };
38
+ }
39
+ if (target.pathname !== "/oidc/device") {
40
+ return { ok: false, reason: "target_path_mismatch" };
41
+ }
42
+ if (target.hash) {
43
+ return { ok: false, reason: "target_has_hash" };
44
+ }
45
+ return { ok: true, returnUrl: target.href };
46
+ }
47
+ function extractDeviceApprovalFromTargetLink(targetHref, opts) {
48
+ let target;
49
+ try {
50
+ target = new URL(targetHref);
51
+ } catch {
52
+ return { error: "bad_target_uri" };
53
+ }
54
+ if (opts?.expectedIssuerUrl) {
55
+ let opOrigin;
56
+ try {
57
+ opOrigin = new URL(stripTrailingSlashes(opts.expectedIssuerUrl.trim())).origin;
58
+ } catch {
59
+ return { error: "target_origin_mismatch" };
60
+ }
61
+ if (target.origin !== opOrigin) {
62
+ return { error: "target_origin_mismatch" };
63
+ }
64
+ }
65
+ if (target.pathname !== "/oidc/device") {
66
+ return { error: "target_path_mismatch" };
67
+ }
68
+ const userCodeRaw = target.searchParams.get("user_code")?.trim() ?? "";
69
+ const clientIdRaw = target.searchParams.get("client_id")?.trim() ?? "";
70
+ if (!userCodeRaw || !USER_CODE_RE.test(userCodeRaw)) {
71
+ return { error: "invalid_user_code" };
72
+ }
73
+ if (!clientIdRaw?.startsWith("app_")) {
74
+ return { error: "invalid_client_id" };
75
+ }
76
+ if (opts?.expectedPublicClientId && clientIdRaw !== opts.expectedPublicClientId) {
77
+ return { error: "client_id_mismatch" };
78
+ }
79
+ return { userCode: userCodeRaw, publicClientId: clientIdRaw };
80
+ }
81
+
82
+ export { USER_CODE_RE, extractDeviceApprovalFromTargetLink, validateDeviceInitiateLogin };
83
+ //# sourceMappingURL=device-initiate.js.map
84
+ //# sourceMappingURL=device-initiate.js.map
@@ -0,0 +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"]}
package/dist/device.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as oauth4webapi from 'oauth4webapi';
2
- import { F as FetchLike } from './types-W9PJAspR.cjs';
2
+ import { F as FetchLike } from './types-rKzVXvMu.cjs';
3
3
 
4
4
  interface PollDeviceTokenOptions {
5
5
  issuerUrl: string;
package/dist/device.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as oauth4webapi from 'oauth4webapi';
2
- import { F as FetchLike } from './types-W9PJAspR.js';
2
+ import { F as FetchLike } from './types-rKzVXvMu.js';
3
3
 
4
4
  interface PollDeviceTokenOptions {
5
5
  issuerUrl: string;