@k9o/gate 1.1.1 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 — 2026-10-26
4
+
5
+ The SDK now matches the gateway routes exactly.
6
+
7
+ ### Changed
8
+
9
+ - `signOut` requires `{ refreshToken }` and returns `{ revoked }`.
10
+ - `whoami` returns the end user record itself.
11
+ - `refreshToken` returns `{ accessToken, expiresAt }`. The refresh token is not rotated.
12
+ - `passwordless.requestOtp` returns `{ otp: { id, expiresAt }, debugCode? }`.
13
+ - `passwordless.verifyOtp` returns `{ verified, session?, endUserId? }`. `session` is set only for the `signin` purpose.
14
+ - `passwordless.requestMagicLink` returns `{ magicLink: { sent, expiresAt? }, debugLink? }`.
15
+ - `passwordless.verifyMagicLink` returns `{ redeemed, session, endUserId }`.
16
+ - `verifyJwt({ jwksUrl })` reads the gateway's envelope-wrapped key set. The route is `GET /@{projectSlug}/auth/jwks`.
17
+ - Regional host lookup works for any `<org>.api.<apex>` host. No apex is hardcoded.
18
+
19
+ ### Removed
20
+
21
+ - `verifySession` and `hosted`: the gateway has no route for them.
22
+
23
+ ### Added
24
+
25
+ - `fetch` option on `verifyJwt` and the exported `fetchJwks` helper.
26
+
27
+ ## 1.2.0 — 2026-10-25
28
+
29
+ - The README now introduces the Kilimanjaro products (Keep, Pulse, Gate, Kibo). No change to the public API.
30
+
3
31
  ## 1.1.1 — 2026-10-25
4
32
 
5
33
  - Documentation links corrected: the documentation hub is https://docs.kilimanjaro.dev, with the website, platform API and console listed in a Links section. Package metadata points at the same places.
package/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Identity for your app's end users on Kilimanjaro. Sign-up, sign-in, sessions, one-time codes, magic links and JWT verification.
4
4
 
5
+ Kilimanjaro is the AI-native foundation for building critical applications. Its products are Keep (AI Security and Governance),
6
+ Pulse (Data Intelligence), Gate (Customer Identity and Access Management) and Kibo (Keys and Secrets Management).
7
+
5
8
  ## Contents
6
9
 
7
10
  - [Links](#links)
@@ -83,18 +86,44 @@ const signedUp = await gate.signUp({ email: "grace@example.com", password: "anot
83
86
  const session = signedUp.data.session;
84
87
 
85
88
  const refreshed = await gate.refreshToken({ refreshToken: session.refreshToken });
89
+ console.log(Object.keys(refreshed.data));
90
+
91
+ const revoked = await gate.signOut({ refreshToken: session.refreshToken });
92
+ console.log(revoked.data.revoked);
93
+ ```
94
+
95
+ ```text
96
+ [ 'accessToken', 'expiresAt' ]
97
+ true
86
98
  ```
87
99
 
88
- - Passwordless sign-in uses `passwordless.requestOtp` then `passwordless.verifyOtp`.
89
- - Browser sign-in with a provider uses `signInWithProvider`.
90
- - See [Methods](#methods) for everything else.
100
+ - `refreshToken` returns a new access token. The refresh token is not rotated, so keep using the one you have.
101
+ - `signOut` requires the refresh token and revokes it.
102
+ - `whoami()` returns the signed-in user record itself.
103
+
104
+ ### Passwordless
105
+
106
+ Request a one-time code, then redeem it. A code issued for the default `signin` purpose returns a session.
91
107
 
92
108
  ```ts
93
- await gate.passwordless.requestOtp({ email: "ada@example.com" });
109
+ const sent = await gate.passwordless.requestOtp({ email: "ada@example.com" });
110
+ console.log(sent.data.otp.id);
111
+
94
112
  const verified = await gate.passwordless.verifyOtp({ email: "ada@example.com", code: "123456" });
113
+ console.log(verified.data.verified, verified.data.session?.accessToken !== undefined);
95
114
  ```
96
115
 
97
- A magic-link `redirectUrl` must exactly match an entry in the project's allowlist.
116
+ ```text
117
+ otp_01j9x2
118
+ true true
119
+ ```
120
+
121
+ - `requestOtp` returns `{ otp: { id, expiresAt } }`. `debugCode` appears only on a development deployment.
122
+ - `verifyOtp` returns `{ verified, session?, endUserId? }`. Other purposes return `verified` only.
123
+ - `requestMagicLink` returns `{ magicLink: { sent, expiresAt? } }`, and `debugLink` on a development deployment.
124
+ - `verifyMagicLink({ token })` returns `{ redeemed, session, endUserId }`.
125
+ - A magic-link `redirectUrl` must exactly match an entry in the project's allowlist.
126
+ - Browser sign-in with a provider uses `signInWithProvider`. See [Methods](#methods).
98
127
 
99
128
  ## Verifying tokens
100
129
 
@@ -105,7 +134,7 @@ import { verifyJwt } from "@k9o/gate";
105
134
 
106
135
  const verification = await verifyJwt({
107
136
  token: accessToken,
108
- jwksUrl: "https://acme.api.k9o.io/@member-portal/gate/.well-known/jwks.json",
137
+ jwksUrl: "https://acme.api.k9o.io/@member-portal/auth/jwks",
109
138
  });
110
139
 
111
140
  if (verification.valid) {
@@ -115,6 +144,14 @@ if (verification.valid) {
115
144
  }
116
145
  ```
117
146
 
147
+ ```text
148
+ eu_01j9x2k4m7
149
+ ```
150
+
151
+ - The project key set is at `GET /@{projectSlug}/auth/jwks`. Organization keys are at `/auth/jwks`.
152
+ - The gateway wraps the key set in its `{ data, meta }` envelope. `verifyJwt` unwraps it for you.
153
+ - To fetch the key set yourself, call `fetchJwks(url)`. Pass `fetch` in the options to use your own `fetch`.
154
+
118
155
  - Set exactly one of `jwksUrl` or `jwks`. See [Verification options](#verification-options).
119
156
  - A failed check returns one of the [Failure reasons](#failure-reasons).
120
157
  - `requireScope(claims, scopes)` throws `RBAC_DENY` if a scope is missing.
@@ -141,9 +178,10 @@ See [Error codes](#error-codes) for the full list. The full wire code is kept in
141
178
 
142
179
  ## Regional hosts
143
180
 
144
- - On an organization host such as `https://acme.api.k9o.io`, the client sends one `HEAD` request first.
181
+ - On an organization host shaped `<org>.api.<apex>`, the client sends one `HEAD` request first.
145
182
  - The request carries no credentials. It finds the regional host for your organization.
146
183
  - All later requests go to that host. The answer is cached.
184
+ - Any apex works, including an internal or air-gapped one. The client hardcodes none.
147
185
  - Any other `baseUrl` is used as given. If the lookup fails, the configured host is used.
148
186
 
149
187
  ## Appendix
@@ -185,22 +223,21 @@ An invalid `mode`, or a missing `projectSlug` or `baseUrl`, throws a `TypeError`
185
223
  | --- | --- |
186
224
  | `signUp({ email, password, displayName? })` | Creates an end user and a session. |
187
225
  | `signIn({ email, password })` | Authenticates with a password. |
188
- | `signOut({ refreshToken? })` | Revokes the current session. |
189
- | `whoami()` | Returns the signed-in user. |
190
- | `refreshToken({ refreshToken })` | Exchanges a refresh token for a new session. |
191
- | `verifySession()` | Checks whether the current session is valid. |
192
- | `hosted()` | Returns the hosted sign-in page and WebAuthn `rpId`. |
226
+ | `signOut({ refreshToken })` | Revokes the session. Returns `{ revoked }`. |
227
+ | `whoami()` | Returns the signed-in end user record. |
228
+ | `refreshToken({ refreshToken })` | Returns `{ accessToken, expiresAt }`. The refresh token is not rotated. |
193
229
  | `signInWithProvider({ provider, redirectUri })` | Sends the browser to a provider. |
194
230
  | `parseSignInCallback(hash)` | Reads tokens from the redirect URL fragment. |
195
- | `passwordless.requestOtp({ email, purpose? })` | Emails a one-time code. |
196
- | `passwordless.verifyOtp({ email, code, purpose? })` | Redeems the code and returns a session. |
197
- | `passwordless.requestMagicLink({ email, redirectUrl? })` | Emails a magic link. |
198
- | `passwordless.verifyMagicLink(input)` | Redeems the link and returns a session. |
231
+ | `passwordless.requestOtp({ email, purpose? })` | Emails a code. Returns `{ otp: { id, expiresAt }, debugCode? }`. |
232
+ | `passwordless.verifyOtp({ email, code, purpose? })` | Returns `{ verified, session?, endUserId? }`. |
233
+ | `passwordless.requestMagicLink({ email, redirectUrl? })` | Returns `{ magicLink: { sent, expiresAt? }, debugLink? }`. |
234
+ | `passwordless.verifyMagicLink({ token })` | Returns `{ redeemed, session, endUserId }`. |
235
+ | `passwordless.parseMagicLinkCallback(search)` | Reads the token from the redirect query string. |
199
236
  | `identityProviders` | Project admins create end-user identity providers. |
200
237
  | `magicLinkRedirects` | Project admins manage the redirect allowlist. |
201
238
 
202
239
  - `provider` is `"google"`, `"github"`, `"microsoft"`, `"oidc"` or `"saml"`.
203
- - Generic OIDC is not wired yet. Use a hosted provider.
240
+ - `session` is returned by `verifyOtp` only for the default `signin` purpose.
204
241
 
205
242
  ### Token helpers
206
243
 
@@ -209,14 +246,16 @@ An invalid `mode`, or a missing `projectSlug` or `baseUrl`, throws a `TypeError`
209
246
  | `verifyJwt(options)` | Verifies a token and returns a result. |
210
247
  | `extractClaims(token)` | Decodes claims without verifying them. |
211
248
  | `requireScope(claims, scopes)` | Throws `RBAC_DENY` if a scope is missing. |
249
+ | `fetchJwks(url, fetch?)` | Reads a key set from the gateway and unwraps the envelope. |
212
250
 
213
251
  ### Verification options
214
252
 
215
253
  | Option | Meaning |
216
254
  | --- | --- |
217
255
  | `token` | The JWT to verify. |
218
- | `jwksUrl` | Remote key set URL. Set this or `jwks`. |
256
+ | `jwksUrl` | Key set URL, such as `/@{projectSlug}/auth/jwks`. Set this or `jwks`. |
219
257
  | `jwks` | Inline key set. Set this or `jwksUrl`. |
258
+ | `fetch` | Custom `fetch` used to read `jwksUrl`. |
220
259
  | `expectedIssuer` | Required `iss` claim. |
221
260
  | `expectedAudience` | Required `aud` claim. |
222
261
  | `allowedAlgorithms` | Defaults to `RS256` and `ES256`. |
@@ -269,7 +308,7 @@ Codes have no `K9O_` prefix.
269
308
  | Package | What it does |
270
309
  | --- | --- |
271
310
  | `@k9o/core` | The full platform client. Exposes this package as `client.gate`. |
272
- | `@k9o/extensions` | Typed bindings for platform extensions. |
311
+ | `@k9o/extensions` | Placeholder. Its methods reject until the platform serves extension calls. |
273
312
  | `@k9o/kibo` | Vault and key operations. |
274
313
 
275
314
  ## License
@@ -48,4 +48,4 @@ export {
48
48
  GateError,
49
49
  gateErrorFromWire
50
50
  };
51
- //# sourceMappingURL=chunk-JEV37YEC.js.map
51
+ //# sourceMappingURL=chunk-7EED6YWR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Gate error shapes. `GateError` is `KError`-compatible —\n * downstream catch blocks can `instanceof GateError`-narrow without\n * importing the gateway's `KError` class directly (the SDK is a customer-\n * facing dependency; reaching across to the gateway's internal error\n * type would be a layering violation).\n *\n * The taxonomy below is the closed set; additions follow the\n * error-catalogue freeze cycle.\n */\n\n/**\n * Closed-taxonomy error code. Strings are the wire format —\n * camelCase-cased KError families per the convention.\n */\nexport type GateErrorCode =\n | \"UNAUTHENTICATED\"\n | \"INVALID_FIELD_VALUE\"\n | \"EMAIL_ALREADY_REGISTERED\"\n | \"INVALID_CREDENTIALS\"\n | \"SESSION_EXPIRED\"\n | \"REFRESH_TOKEN_INVALID\"\n | \"TIER_FEATURE_GATED\"\n | \"RBAC_DENY\"\n // FORBIDDEN + NOT_FOUND are surfaced by the gate.idpProviders.*\n // sub-namespaces (project-membership gate + resource-existence checks).\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n // GATE_REDIRECT_NOT_ALLOWED — magic-link `redirectUrl` not in the project's\n // redirect allowlist (open-redirect-safe exact-match, per-project\n // allowlist capability).\n | \"GATE_REDIRECT_NOT_ALLOWED\"\n | \"POLICY_AUTHORING_AUTHORITY_INSUFFICIENT\"\n | \"UPSTREAM_UNAVAILABLE\"\n | \"INTERNAL_ERROR\";\n\n/**\n * Surface error thrown by the SDK. Mirrors the gateway's `KError` shape\n * `code` (closed taxonomy), `message` (human-readable),\n * `context` (structured per-code payload), `recommendation` (action the\n * caller can take, or null when there's no canonical recommendation).\n */\nexport class GateError extends Error {\n readonly code: GateErrorCode;\n readonly context: Readonly<Record<string, unknown>>;\n readonly recommendation: string | null;\n /** HTTP status the gateway returned (if the error came from a wire response). */\n readonly status: number | null;\n\n constructor(init: {\n code: GateErrorCode;\n message: string;\n context?: Record<string, unknown>;\n recommendation?: string | null;\n status?: number | null;\n }) {\n super(init.message);\n this.name = \"GateError\";\n this.code = init.code;\n this.context = Object.freeze({ ...(init.context ?? {}) });\n this.recommendation = init.recommendation ?? null;\n this.status = init.status ?? null;\n }\n}\n\n/**\n * Wire-shape sentinel matching the gateway's `KError` JSON payload. The\n * client uses this to recognise gateway responses and rehydrate them\n * into a `GateError` instance with the right code + context preserved.\n */\nexport type KErrorWire = {\n code: string;\n message: string;\n context?: Record<string, unknown>;\n recommendation?: string | null;\n};\n\n/**\n * Narrow a wire `KError` to a strongly-typed `GateError`. Unknown codes\n * pass through as `INTERNAL_ERROR` with the original code preserved in\n * `context.originalCode` so consumers don't lose telemetry when the\n * server-side taxonomy outpaces the SDK's bundled enum.\n */\nexport function gateErrorFromWire(wire: KErrorWire, status: number): GateError {\n const knownCodes: ReadonlyArray<GateErrorCode> = [\n \"UNAUTHENTICATED\",\n \"INVALID_FIELD_VALUE\",\n \"EMAIL_ALREADY_REGISTERED\",\n \"INVALID_CREDENTIALS\",\n \"SESSION_EXPIRED\",\n \"REFRESH_TOKEN_INVALID\",\n \"TIER_FEATURE_GATED\",\n \"RBAC_DENY\",\n \"FORBIDDEN\",\n \"NOT_FOUND\",\n \"GATE_REDIRECT_NOT_ALLOWED\",\n \"POLICY_AUTHORING_AUTHORITY_INSUFFICIENT\",\n \"UPSTREAM_UNAVAILABLE\",\n \"INTERNAL_ERROR\",\n ];\n // Wire codes are platform-namespaced (`K9O_*`);\n // strip the prefix to match the SDK's bundled bare-code enum. The full wire\n // code is always preserved in `context.originalCode` for telemetry.\n const stripped = wire.code.replace(/^K9O_/, \"\");\n const isKnown = (knownCodes as ReadonlyArray<string>).includes(stripped);\n const code: GateErrorCode = isKnown ? (stripped as GateErrorCode) : \"INTERNAL_ERROR\";\n const context = { ...(wire.context ?? {}), originalCode: wire.code };\n return new GateError({\n code,\n message: wire.message,\n context,\n recommendation: wire.recommendation ?? null,\n status,\n });\n}\n"],"mappings":";AA0CO,IAAM,YAAN,cAAwB,MAAM;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EAET,YAAY,MAMT;AACD,UAAM,KAAK,OAAO;AAClB,SAAK,OAAO;AACZ,SAAK,OAAO,KAAK;AACjB,SAAK,UAAU,OAAO,OAAO,EAAE,GAAI,KAAK,WAAW,CAAC,EAAG,CAAC;AACxD,SAAK,iBAAiB,KAAK,kBAAkB;AAC7C,SAAK,SAAS,KAAK,UAAU;AAAA,EAC/B;AACF;AAoBO,SAAS,kBAAkB,MAAkB,QAA2B;AAC7E,QAAM,aAA2C;AAAA,IAC/C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAIA,QAAM,WAAW,KAAK,KAAK,QAAQ,SAAS,EAAE;AAC9C,QAAM,UAAW,WAAqC,SAAS,QAAQ;AACvE,QAAM,OAAsB,UAAW,WAA6B;AACpE,QAAM,UAAU,EAAE,GAAI,KAAK,WAAW,CAAC,GAAI,cAAc,KAAK,KAAK;AACnE,SAAO,IAAI,UAAU;AAAA,IACnB;AAAA,IACA,SAAS,KAAK;AAAA,IACd;AAAA,IACA,gBAAgB,KAAK,kBAAkB;AAAA,IACvC;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  GateError
3
- } from "./chunk-JEV37YEC.js";
3
+ } from "./chunk-7EED6YWR.js";
4
4
 
5
5
  // src/jwt.ts
6
- import { createRemoteJWKSet, jwtVerify, errors as joseErrors } from "jose";
6
+ import { jwtVerify, errors as joseErrors } from "jose";
7
7
  var DEFAULT_ALLOWED_ALGORITHMS = ["RS256", "ES256"];
8
8
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
9
9
  async function verifyJwt(opts) {
@@ -27,7 +27,7 @@ async function verifyJwt(opts) {
27
27
  const clockTolerance = opts.clockToleranceSeconds ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;
28
28
  let keyResolver;
29
29
  if (opts.jwksUrl !== void 0) {
30
- keyResolver = createRemoteJWKSet(new URL(opts.jwksUrl));
30
+ keyResolver = makeLocalJwksResolver(await fetchJwks(opts.jwksUrl, opts.fetch));
31
31
  } else {
32
32
  keyResolver = makeLocalJwksResolver(opts.jwks);
33
33
  }
@@ -77,6 +77,42 @@ function requireScope(claims, requiredScopes) {
77
77
  });
78
78
  }
79
79
  }
80
+ function isJwks(v) {
81
+ return v !== null && typeof v === "object" && Array.isArray(v.keys);
82
+ }
83
+ async function fetchJwks(url, fetchImpl = globalThis.fetch) {
84
+ if (typeof fetchImpl !== "function") {
85
+ throw new GateError({
86
+ code: "INTERNAL_ERROR",
87
+ message: "No fetch implementation available to read the JWKS",
88
+ context: { url },
89
+ recommendation: null
90
+ });
91
+ }
92
+ let body;
93
+ let status;
94
+ try {
95
+ const res = await fetchImpl(url, { method: "GET", headers: { accept: "application/json" } });
96
+ status = res.status;
97
+ body = await res.json();
98
+ } catch (e) {
99
+ throw new GateError({
100
+ code: "UPSTREAM_UNAVAILABLE",
101
+ message: `Could not read the JWKS at ${url}: ${e instanceof Error ? e.message : String(e)}`,
102
+ context: { url },
103
+ recommendation: null
104
+ });
105
+ }
106
+ const inner = body !== null && typeof body === "object" ? body.data : void 0;
107
+ if (isJwks(inner)) return inner;
108
+ if (isJwks(body)) return body;
109
+ throw new GateError({
110
+ code: "UPSTREAM_UNAVAILABLE",
111
+ message: `The JWKS route at ${url} answered ${status} without a key set`,
112
+ context: { url, status },
113
+ recommendation: null
114
+ });
115
+ }
80
116
  function classifyJoseFailure(e) {
81
117
  if (e instanceof joseErrors.JWSSignatureVerificationFailed) return "INVALID_SIGNATURE";
82
118
  if (e instanceof joseErrors.JWTExpired) return "EXPIRED";
@@ -120,6 +156,7 @@ function makeLocalJwksResolver(jwks) {
120
156
  export {
121
157
  verifyJwt,
122
158
  extractClaims,
123
- requireScope
159
+ requireScope,
160
+ fetchJwks
124
161
  };
125
- //# sourceMappingURL=chunk-YINWOWJC.js.map
162
+ //# sourceMappingURL=chunk-OJQYLAGZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/jwt.ts"],"sourcesContent":["/**\n * JWT verification primitives for apps that consume Gate-issued tokens without the rest of the SDK: verify the\n * signature, read the claims, check scopes. Backed by `jose`.\n */\n\nimport { jwtVerify, errors as joseErrors } from \"jose\";\nimport type { GateJwtClaims, VerifyJwtFailure, VerifyJwtResult } from \"./types.ts\";\nimport { GateError } from \"./errors.ts\";\n\n/**\n * Configuration for `verifyJwt`. Signing keys come from either the gateway's JWKS route (`jwksUrl`) or an inline key\n * set (`jwks`, for tests and air-gapped deployments).\n */\nexport type VerifyJwtOptions = {\n token: string;\n /**\n * URL of the gateway's JWKS route: `https://<org>.api.<apex>/@<projectSlug>/auth/jwks` for a project's end-user\n * tokens (`/auth/jwks` on the org host for org keys). The gateway wraps the key set in its response envelope\n * (`{ data: { keys }, meta }`), which `jose` cannot read directly; this helper fetches the URL and unwraps it.\n * Either `jwksUrl` or `jwks` must be set; not both.\n */\n jwksUrl?: string;\n /** Inline JWK set, the standard `{ keys: [...] }` object. */\n jwks?: { keys: ReadonlyArray<Record<string, unknown>> };\n /** Fetch implementation used for `jwksUrl`. Defaults to the global `fetch`. */\n fetch?: typeof globalThis.fetch;\n /** Expected issuer (`iss` claim). Strict comparison. */\n expectedIssuer?: string;\n /** Expected audience (`aud` claim). */\n expectedAudience?: string;\n /**\n * Allowed signing algorithms. Defaults to `[\"RS256\", \"ES256\"]`. Anything outside this set is rejected (defense\n * against algorithm confusion).\n */\n allowedAlgorithms?: ReadonlyArray<string>;\n /** Clock-skew tolerance in seconds. Defaults to 30. Applies to `exp` and `nbf`. */\n clockToleranceSeconds?: number;\n};\n\nconst DEFAULT_ALLOWED_ALGORITHMS = [\"RS256\", \"ES256\"] as const;\nconst DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;\n\n/**\n * Verify a Gate-issued JWT. Returns a typed result; does NOT throw on\n * verification failure (a `valid: false` result carries the reason).\n *\n * Throws `GateError` only when:\n * - `jwksUrl` and `jwks` are both unset or both set\n * - the JWKS fetch fails (UPSTREAM_UNAVAILABLE) or answers something that is not a key set\n */\nexport async function verifyJwt(opts: VerifyJwtOptions): Promise<VerifyJwtResult> {\n if (opts.jwksUrl !== undefined && opts.jwks !== undefined) {\n throw new GateError({\n code: \"INVALID_FIELD_VALUE\",\n message: \"verifyJwt: pass either jwksUrl OR jwks, not both\",\n context: {},\n recommendation: null,\n });\n }\n if (opts.jwksUrl === undefined && opts.jwks === undefined) {\n throw new GateError({\n code: \"INVALID_FIELD_VALUE\",\n message: \"verifyJwt: must pass exactly one of {jwksUrl, jwks}\",\n context: {},\n recommendation: null,\n });\n }\n\n const allowedAlgorithms = opts.allowedAlgorithms ?? DEFAULT_ALLOWED_ALGORITHMS;\n const clockTolerance = opts.clockToleranceSeconds ?? DEFAULT_CLOCK_TOLERANCE_SECONDS;\n\n let keyResolver: Parameters<typeof jwtVerify>[1];\n if (opts.jwksUrl !== undefined) {\n keyResolver = makeLocalJwksResolver(await fetchJwks(opts.jwksUrl, opts.fetch));\n } else {\n keyResolver = makeLocalJwksResolver(opts.jwks!);\n }\n\n try {\n const result = await jwtVerify(opts.token, keyResolver, {\n ...(opts.expectedIssuer !== undefined ? { issuer: opts.expectedIssuer } : {}),\n ...(opts.expectedAudience !== undefined ? { audience: opts.expectedAudience } : {}),\n algorithms: allowedAlgorithms as string[],\n clockTolerance,\n });\n return { valid: true, claims: result.payload as GateJwtClaims };\n } catch (e) {\n return { valid: false, reason: classifyJoseFailure(e) };\n }\n}\n\n/**\n * Extract claims from a JWT WITHOUT verifying the signature. Used by\n * SDK consumers that want to read claim data in client code where\n * signature verification has already happened at a trusted hop (e.g.,\n * a reverse proxy). Caller is responsible for confirming the upstream\n * trust path before consuming these claims for security decisions.\n *\n * Throws `GateError` on malformed input.\n */\nexport function extractClaims(token: string): GateJwtClaims {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new GateError({\n code: \"INVALID_FIELD_VALUE\",\n message: `Malformed JWT: expected 3 dot-separated parts, got ${parts.length}`,\n context: {},\n recommendation: null,\n });\n }\n try {\n const payloadJson = base64UrlDecode(parts[1]!);\n return JSON.parse(payloadJson) as GateJwtClaims;\n } catch (e) {\n throw new GateError({\n code: \"INVALID_FIELD_VALUE\",\n message: `Malformed JWT payload: ${e instanceof Error ? e.message : String(e)}`,\n context: {},\n recommendation: null,\n });\n }\n}\n\n/**\n * Assert that the given claim set contains all of the required scopes.\n * Throws `GateError` with `code: \"RBAC_DENY\"` if any scope is missing.\n * Returns nothing on success — caller uses the throw-or-pass shape.\n */\nexport function requireScope(\n claims: GateJwtClaims,\n requiredScopes: ReadonlyArray<string>,\n): void {\n const present = new Set(claims.scopes ?? []);\n const missing = requiredScopes.filter((s) => !present.has(s));\n if (missing.length > 0) {\n throw new GateError({\n code: \"RBAC_DENY\",\n message: `Required scopes missing: ${missing.join(\", \")}`,\n context: { missingScopes: missing, presentScopes: Array.from(present) },\n recommendation: null,\n });\n }\n}\n\n// ----------------------------------------------------------------------\n// Internals\n// ----------------------------------------------------------------------\n\ntype Jwks = { keys: ReadonlyArray<Record<string, unknown>> };\n\nfunction isJwks(v: unknown): v is Jwks {\n return v !== null && typeof v === \"object\" && Array.isArray((v as { keys?: unknown }).keys);\n}\n\n/**\n * Fetch a JWKS from the gateway. The route answers `{ data: { keys }, meta }`; a bare `{ keys }` (any other JWKS\n * host) is accepted too.\n */\nexport async function fetchJwks(url: string, fetchImpl: typeof globalThis.fetch | undefined = globalThis.fetch): Promise<Jwks> {\n if (typeof fetchImpl !== \"function\") {\n throw new GateError({\n code: \"INTERNAL_ERROR\",\n message: \"No fetch implementation available to read the JWKS\",\n context: { url },\n recommendation: null,\n });\n }\n let body: unknown;\n let status: number;\n try {\n const res = await fetchImpl(url, { method: \"GET\", headers: { accept: \"application/json\" } });\n status = res.status;\n body = await res.json();\n } catch (e) {\n throw new GateError({\n code: \"UPSTREAM_UNAVAILABLE\",\n message: `Could not read the JWKS at ${url}: ${e instanceof Error ? e.message : String(e)}`,\n context: { url },\n recommendation: null,\n });\n }\n const inner = body !== null && typeof body === \"object\" ? (body as { data?: unknown }).data : undefined;\n if (isJwks(inner)) return inner;\n if (isJwks(body)) return body;\n throw new GateError({\n code: \"UPSTREAM_UNAVAILABLE\",\n message: `The JWKS route at ${url} answered ${status} without a key set`,\n context: { url, status },\n recommendation: null,\n });\n}\n\nfunction classifyJoseFailure(e: unknown): VerifyJwtFailure {\n // Map `jose`'s typed errors to our closed reason set per `VerifyJwtFailure`.\n if (e instanceof joseErrors.JWSSignatureVerificationFailed) return \"INVALID_SIGNATURE\";\n if (e instanceof joseErrors.JWTExpired) return \"EXPIRED\";\n if (e instanceof joseErrors.JWTClaimValidationFailed) {\n const claim = (e as { claim?: string }).claim;\n if (claim === \"aud\") return \"AUDIENCE_MISMATCH\";\n if (claim === \"iss\") return \"ISSUER_MISMATCH\";\n if (claim === \"nbf\") return \"NOT_YET_VALID\";\n return \"MALFORMED\";\n }\n if (e instanceof joseErrors.JOSEAlgNotAllowed) return \"ALGORITHM_NOT_ALLOWED\";\n // Fallback — `jose` raises other errors (parse failures, KID mismatches,\n // etc.) that we lump into `MALFORMED` rather than leaking the per-version\n // taxonomy.\n return \"MALFORMED\";\n}\n\nfunction base64UrlDecode(input: string): string {\n // Pad to a multiple of 4 + swap urlsafe alphabet -> standard alphabet.\n const padded = input + \"=\".repeat((4 - (input.length % 4)) % 4);\n const standard = padded.replace(/-/g, \"+\").replace(/_/g, \"/\");\n // Bun + Node 18+ + browsers all expose `atob`.\n const binary = globalThis.atob(standard);\n // Reassemble bytes back to UTF-8 text.\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return new TextDecoder(\"utf-8\").decode(bytes);\n}\n\n/** Build a key resolver for `jose.jwtVerify` from a JWKS object. */\nfunction makeLocalJwksResolver(\n jwks: { keys: ReadonlyArray<Record<string, unknown>> },\n): Parameters<typeof jwtVerify>[1] {\n // The `jose` resolver signature is `(header, token) => Promise<KeyLike>`\n // — pull the kid from the header, match against the JWKS, and import.\n return async (header) => {\n const { importJWK } = await import(\"jose\");\n const kid = (header as { kid?: string }).kid;\n let match: Record<string, unknown> | undefined;\n if (kid !== undefined) {\n match = jwks.keys.find((k) => k[\"kid\"] === kid);\n }\n if (match === undefined && jwks.keys.length === 1) {\n // Single-key JWKS — return the only key. Tests typically ship one\n // key in the inline set and may not bother with `kid`.\n match = jwks.keys[0];\n }\n if (match === undefined) {\n throw new joseErrors.JWKSNoMatchingKey(\"No matching key in inline JWKS for kid\");\n }\n const alg = (header as { alg?: string }).alg ?? (match[\"alg\"] as string | undefined);\n return importJWK(match as unknown as Parameters<typeof importJWK>[0], alg);\n };\n}\n"],"mappings":"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","names":[]}
@@ -38,4 +38,4 @@ export {
38
38
  getGateConfig,
39
39
  resetGateConfig
40
40
  };
41
- //# sourceMappingURL=chunk-XP6U5QKF.js.map
41
+ //# sourceMappingURL=chunk-RPE5N37W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mode.ts"],"sourcesContent":["/**\n * Standalone vs Native mode wiring.\n *\n * The SDK surface is identical across modes; the mode only tells the gateway how to route consumption. The SDK\n * records it on every request in the `X-Gate-Mode` header and does not branch on it at call time.\n *\n * Function names, signatures, response envelope and error taxonomy are the same in both modes.\n */\n\n/** Two orthogonal deployment shapes; they are not interleaved. */\nexport type GateMode = \"standalone\" | \"native\";\n\n/**\n * Gate SDK configuration. Returned by `getGateConfig()` and consumed\n * internally by the HTTP client + JWT verifier. Set via `configureGate`\n * at process boot or per-test in unit tests.\n */\nexport type GateConfig = {\n mode: GateMode;\n /** Project slug (e.g. `partner-console-staging`). Routes resolve to `/@<slug>/gate/...`. Required. */\n projectSlug: string;\n /** Gateway base URL, e.g. `https://<orgSlug>.api.<apex>` */\n baseUrl: string;\n /**\n * Optional partner-credentialled JWT for Standalone mode — used as the\n * default Authorization header when no per-call bearer is supplied.\n * Not required in Native mode; the gateway resolves credentials from\n * the standard project context.\n */\n partnerToken?: string;\n /**\n * Optional fetch override for testing + non-browser environments. The\n * SDK defaults to global `fetch` (available in Bun + Node 18+ +\n * browsers) when not set.\n */\n fetch?: typeof fetch;\n};\n\nlet _config: GateConfig | null = null;\n\n/**\n * Initialise the SDK. Must be called before any `gate.*` invocation.\n * Re-calling replaces the prior config wholesale (no per-field merge —\n * keeps the config-set-at-boot ergonomic clean).\n */\nexport function configureGate(config: GateConfig): void {\n validateGateConfig(config);\n _config = { ...config };\n}\n\n/**\n * Validate a config shape. Exposed for `createGateClient` so per-instance\n * configs go through identical checks as the singleton.\n */\nexport function validateGateConfig(config: GateConfig): void {\n if (config.mode !== \"standalone\" && config.mode !== \"native\") {\n throw new TypeError(\n `Invalid Gate mode \"${config.mode as string}\"; expected \"standalone\" or \"native\"`,\n );\n }\n const hasSlug = typeof config.projectSlug === \"string\" && config.projectSlug.length > 0;\n if (!hasSlug) {\n throw new TypeError(\n `Gate config missing projectSlug (required, slug grammar — project ids are internal-only and never customer-facing)`,\n );\n }\n if (typeof config.baseUrl !== \"string\" || config.baseUrl.length === 0) {\n throw new TypeError(`Invalid baseUrl \"${config.baseUrl}\"; expected non-empty string`);\n }\n}\n\n/**\n * Read the active config. Throws when called before `configureGate(...)`.\n * Tests can call `resetGateConfig()` to clear between cases.\n */\nexport function getGateConfig(): GateConfig {\n if (_config === null) {\n throw new Error(\n \"@k9o/gate: SDK is not configured. Call configureGate({ mode, projectSlug, baseUrl }) before invoking gate functions.\",\n );\n }\n return _config;\n}\n\n/** Reset the singleton — used by tests + by app-shutdown teardown paths. */\nexport function resetGateConfig(): void {\n _config = null;\n}\n"],"mappings":";AAsCA,IAAI,UAA6B;AAO1B,SAAS,cAAc,QAA0B;AACtD,qBAAmB,MAAM;AACzB,YAAU,EAAE,GAAG,OAAO;AACxB;AAMO,SAAS,mBAAmB,QAA0B;AAC3D,MAAI,OAAO,SAAS,gBAAgB,OAAO,SAAS,UAAU;AAC5D,UAAM,IAAI;AAAA,MACR,sBAAsB,OAAO,IAAc;AAAA,IAC7C;AAAA,EACF;AACA,QAAM,UAAU,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,SAAS;AACtF,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,OAAO,OAAO,YAAY,YAAY,OAAO,QAAQ,WAAW,GAAG;AACrE,UAAM,IAAI,UAAU,oBAAoB,OAAO,OAAO,8BAA8B;AAAA,EACtF;AACF;AAMO,SAAS,gBAA4B;AAC1C,MAAI,YAAY,MAAM;AACpB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,kBAAwB;AACtC,YAAU;AACZ;","names":[]}
package/dist/errors.d.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  type GateErrorCode = "UNAUTHENTICATED" | "INVALID_FIELD_VALUE" | "EMAIL_ALREADY_REGISTERED" | "INVALID_CREDENTIALS" | "SESSION_EXPIRED" | "REFRESH_TOKEN_INVALID" | "TIER_FEATURE_GATED" | "RBAC_DENY" | "FORBIDDEN" | "NOT_FOUND" | "GATE_REDIRECT_NOT_ALLOWED" | "POLICY_AUTHORING_AUTHORITY_INSUFFICIENT" | "UPSTREAM_UNAVAILABLE" | "INTERNAL_ERROR";
16
16
  /**
17
17
  * Surface error thrown by the SDK. Mirrors the gateway's `KError` shape
18
- * — `code` (closed taxonomy), `message` (human-readable),
18
+ * `code` (closed taxonomy), `message` (human-readable),
19
19
  * `context` (structured per-code payload), `recommendation` (action the
20
20
  * caller can take, or null when there's no canonical recommendation).
21
21
  */
package/dist/errors.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  GateError,
3
3
  gateErrorFromWire
4
- } from "./chunk-JEV37YEC.js";
4
+ } from "./chunk-7EED6YWR.js";
5
5
  export {
6
6
  GateError,
7
7
  gateErrorFromWire