@andco/sdk 0.0.2 → 0.0.4

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 (83) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +96 -54
  3. package/dist/auth.d.ts +8 -3
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +32 -23
  6. package/dist/browser/controller.d.ts +55 -1
  7. package/dist/browser/controller.d.ts.map +1 -1
  8. package/dist/browser/controller.js +166 -9
  9. package/dist/browser/frame.d.ts +4 -3
  10. package/dist/browser/frame.d.ts.map +1 -1
  11. package/dist/browser/frame.js +12 -20
  12. package/dist/browser/index.d.ts +18 -6
  13. package/dist/browser/index.d.ts.map +1 -1
  14. package/dist/browser/index.js +64 -29
  15. package/dist/browser/popup.d.ts +6 -1
  16. package/dist/browser/popup.d.ts.map +1 -1
  17. package/dist/browser/popup.js +37 -8
  18. package/dist/cli/index.d.ts +4 -2
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +10 -3
  21. package/dist/cli/server.d.ts +5 -0
  22. package/dist/cli/server.d.ts.map +1 -1
  23. package/dist/cli/server.js +5 -0
  24. package/dist/client.d.ts +66 -19
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +125 -74
  27. package/dist/config.d.ts +2 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +1 -0
  30. package/dist/credentials.d.ts +58 -4
  31. package/dist/credentials.d.ts.map +1 -1
  32. package/dist/credentials.js +0 -0
  33. package/dist/errors.d.ts +23 -21
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +18 -20
  36. package/dist/globals.d.ts +25 -0
  37. package/dist/globals.d.ts.map +1 -0
  38. package/dist/globals.js +15 -0
  39. package/dist/grants-api.d.ts +34 -0
  40. package/dist/grants-api.d.ts.map +1 -0
  41. package/dist/grants-api.js +48 -0
  42. package/dist/grants.d.ts +16 -0
  43. package/dist/grants.d.ts.map +1 -0
  44. package/dist/grants.js +13 -0
  45. package/dist/index.d.ts +12 -6
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -2
  48. package/dist/inflight.d.ts +31 -0
  49. package/dist/inflight.d.ts.map +1 -0
  50. package/dist/inflight.js +26 -0
  51. package/dist/intents.d.ts +386 -40
  52. package/dist/intents.d.ts.map +1 -1
  53. package/dist/intents.js +712 -53
  54. package/dist/oauth.d.ts +55 -6
  55. package/dist/oauth.d.ts.map +1 -1
  56. package/dist/oauth.js +86 -57
  57. package/dist/presenter.d.ts +59 -11
  58. package/dist/presenter.d.ts.map +1 -1
  59. package/dist/presenter.js +40 -1
  60. package/dist/resource.d.ts +109 -0
  61. package/dist/resource.d.ts.map +1 -0
  62. package/dist/resource.js +151 -0
  63. package/dist/rest.d.ts +23 -4
  64. package/dist/rest.d.ts.map +1 -1
  65. package/dist/rest.js +46 -5
  66. package/dist/server/index.d.ts +3 -0
  67. package/dist/server/index.d.ts.map +1 -1
  68. package/dist/server/index.js +1 -0
  69. package/dist/server-metadata.generated.d.ts.map +1 -1
  70. package/dist/server-metadata.generated.js +8 -4
  71. package/dist/service.d.ts +109 -0
  72. package/dist/service.d.ts.map +1 -0
  73. package/dist/service.js +241 -0
  74. package/dist/session-store.d.ts +12 -4
  75. package/dist/session-store.d.ts.map +1 -1
  76. package/dist/session-store.js +64 -14
  77. package/dist/storage.d.ts +16 -23
  78. package/dist/storage.d.ts.map +1 -1
  79. package/dist/storage.js +27 -25
  80. package/dist/tokens.d.ts +46 -0
  81. package/dist/tokens.d.ts.map +1 -0
  82. package/dist/tokens.js +148 -0
  83. package/package.json +13 -3
package/dist/oauth.d.ts CHANGED
@@ -2,28 +2,51 @@ import * as oidc from "openid-client";
2
2
  import type { AndcoConfig, AndcoScope } from "./config.js";
3
3
  import type { AndcoSession, AndcoUser } from "./credentials.js";
4
4
  import { Result } from "./errors.js";
5
+ import type { AndcoGlobals } from "./globals.js";
5
6
  /** An RFC 9396 authorization detail. Its shape is owned by the Resource Server that defines it. */
6
7
  export type AndcoAuthorizationDetail = {
7
8
  readonly type: string;
8
9
  } & Readonly<Record<string, unknown>>;
9
- /** One Resource Server's contribution to a single authorization request. */
10
+ /**
11
+ * One Resource Server's contribution to a single authorization request.
12
+ *
13
+ * Whose resources it asks for lives in each detail's `subject`, for Andco's types and third-party
14
+ * types alike: absent or `null` is the signed-in Profile, `{ type: "organization" }` lets the person
15
+ * pick one of their Organizations, and an `identifier` fixes it.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const contribution: AndcoResourceAuthorization = {
20
+ * resourceServerId: "00000000-0000-0000-0000-00000000f301",
21
+ * resource: "https://openfactura.localhost/api",
22
+ * scopes: [],
23
+ * authorizationDetails: [
24
+ * { type: "documents", actions: ["read"], subject: { type: "organization", identifier: "2" } },
25
+ * ],
26
+ * };
27
+ * ```
28
+ */
10
29
  export type AndcoResourceAuthorization = {
30
+ /** Registered Resource Server ID carried by this contribution. */
11
31
  readonly resourceServerId: string;
32
+ /** OAuth Resource Indicator; each distinct contribution becomes a repeated `resource` parameter. */
12
33
  readonly resource: string;
13
34
  readonly scopes: readonly AndcoScope[];
14
35
  readonly authorizationDetails?: readonly AndcoAuthorizationDetail[];
15
- readonly orgId?: string;
16
36
  };
17
37
  /** How an authorization request reaches the Authorization Server. */
18
38
  export type AndcoTransportMode = "auto" | "get" | "par";
19
39
  /** Everything a caller may vary for one authorization request. */
20
40
  export type AndcoAuthorizationOptions = {
41
+ /** Registered callback URL. Defaults to the instance's `redirectTo`. */
21
42
  redirectTo?: string | URL;
43
+ /** Requested scopes, merged with resource contributions. Defaults to `initialScopes`. */
22
44
  scopes?: readonly AndcoScope[];
23
45
  /** Immutable contributions from Resource Server Definitions, composed into one request. */
24
46
  authorizations?: readonly AndcoResourceAuthorization[];
47
+ /** Token audiences, merged with resource contributions and deduplicated. */
25
48
  resource?: string | readonly string[];
26
- orgId?: string;
49
+ /** Fine-grained permissions merged with resource contributions; selects PAR in `auto` mode. */
27
50
  authorizationDetails?: readonly AndcoAuthorizationDetail[];
28
51
  /** Opaque destination reference scoped to this OAuth Client. */
29
52
  externalId?: string;
@@ -36,10 +59,15 @@ export type AndcoAuthorizationOptions = {
36
59
  * terminal's memory.
37
60
  */
38
61
  export type AndcoAuthorizationRequest = {
62
+ /** URL to present to the user; may contain a short-lived PAR reference. */
39
63
  authorizationUrl: URL;
64
+ /** Private PKCE verifier; persist with the transaction and never include it in authorization URLs or logs. */
40
65
  codeVerifier: string;
66
+ /** Correlates the callback with this transaction. */
41
67
  state: string;
68
+ /** Exact callback against which the response is validated. */
42
69
  redirectTo: string;
70
+ /** Unix time in milliseconds; callbacks older than ten minutes are rejected. */
43
71
  createdAt: number;
44
72
  };
45
73
  /** RFC 8628 instructions a constrained client displays instead of opening a browser. */
@@ -48,16 +76,21 @@ export type AndcoDeviceAuthorization = {
48
76
  userCode: string;
49
77
  verificationUri: string;
50
78
  verificationUriComplete: string;
79
+ /** Lifetime of the device code, in seconds. */
51
80
  expiresIn: number;
81
+ /** Minimum polling interval from the server, in seconds. */
52
82
  interval: number;
53
83
  /** The server response, kept so polling needs no reconstruction by the caller. */
54
84
  raw: oidc.DeviceAuthorizationResponse;
55
85
  };
86
+ /** OAuth endpoints, client authentication, transport policy, and runtime capabilities. */
56
87
  export type AndcoOAuthOptions = {
57
88
  config: AndcoConfig;
58
- fetch: typeof globalThis.fetch;
89
+ /** Platform capabilities; only `fetch` is read today. `resolveGlobals()` builds one. */
90
+ globals: AndcoGlobals;
59
91
  /** Present for a confidential client, absent for a public one. */
60
92
  clientSecret?: string | null;
93
+ /** Defaults to `auto`: PAR for sensitive parameters or URLs over 2048 characters, otherwise GET. */
61
94
  transport?: AndcoTransportMode;
62
95
  };
63
96
  /**
@@ -117,8 +150,24 @@ export declare class AndcoOAuth {
117
150
  refresh(refreshToken: string): Promise<Result<AndcoSession>>;
118
151
  /** Reads the authenticated subject for one access token. */
119
152
  userInfo(accessToken: string): Promise<Result<AndcoUser>>;
120
- /** Starts RFC 8628 Device Authorization for a runtime that cannot present a browser. */
121
- createDeviceAuthorizationRequest(options?: Pick<AndcoAuthorizationOptions, "scopes" | "authorizations">): Promise<Result<AndcoDeviceAuthorization>>;
153
+ /**
154
+ * Starts RFC 8628 Device Authorization for a runtime that cannot present a browser.
155
+ *
156
+ * Takes the same permissions as {@link createAuthorizationRequest}: scopes, Resource Server
157
+ * contributions (bound to their server through the same plan, with one `resource` parameter
158
+ * each) and loose `authorizationDetails`. They travel in the device request's form body, which
159
+ * the Authorization Server records exactly as it records a pushed request.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * const { data: device } = await andco.oauth.createDeviceAuthorizationRequest({
164
+ * scopes: ["openid", "email"],
165
+ * authorizations: [bank.accounts.authorization({ actions: ["read"], subject: { type: "profile" } })],
166
+ * });
167
+ * console.log(device.verificationUriComplete, device.userCode);
168
+ * ```
169
+ */
170
+ createDeviceAuthorizationRequest(options?: Pick<AndcoAuthorizationOptions, "scopes" | "authorizations" | "authorizationDetails" | "resource">): Promise<Result<AndcoDeviceAuthorization>>;
122
171
  /**
123
172
  * Waits for the user to approve a device authorization, then returns the session.
124
173
  *
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/dist/oauth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createAndCoAuthorizationPlan, decodeJwtPayload } from "@andco/protocol";
1
2
  import * as oidc from "openid-client";
2
3
  import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
3
4
  import { andcoServerMetadata } from "./server-metadata.generated.js";
@@ -27,13 +28,13 @@ const SENSITIVE_PARAMETERS = ["authorization_details", "login_hint"];
27
28
  */
28
29
  export class AndcoOAuth {
29
30
  #config;
30
- #fetch;
31
+ #globals;
31
32
  #clientSecret;
32
33
  #transport;
33
34
  #configuration;
34
35
  constructor(options) {
35
36
  this.#config = options.config;
36
- this.#fetch = options.fetch;
37
+ this.#globals = options.globals;
37
38
  this.#clientSecret = options.clientSecret ?? null;
38
39
  this.#transport = options.transport ?? "auto";
39
40
  }
@@ -56,7 +57,7 @@ export class AndcoOAuth {
56
57
  return Result.ok({ authorizationUrl, codeVerifier, state, redirectTo, createdAt: Date.now() });
57
58
  }
58
59
  catch (cause) {
59
- return Result.fail(AndcoError.from(cause, "authorization_request_failed"));
60
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.AUTHORIZATION_REQUEST_FAILED));
60
61
  }
61
62
  }
62
63
  /**
@@ -79,9 +80,12 @@ export class AndcoOAuth {
79
80
  }
80
81
  const oauthError = callbackUrl.searchParams.get("error");
81
82
  if (oauthError) {
82
- return Result.fail(oauthError.slice(0, 256), {
83
+ // The Authorization Server's own RFC 6749 `error` parameter — a foreign vocabulary, not one
84
+ // of the SDK's own codes, so it is built through the open `AndcoError` constructor rather
85
+ // than `Result.fail`'s closed shorthand.
86
+ return Result.fail(new AndcoError(oauthError.slice(0, 256), {
83
87
  message: callbackUrl.searchParams.get("error_description")?.slice(0, 2_048) ?? oauthError,
84
- });
88
+ }));
85
89
  }
86
90
  else if (!callbackUrl.searchParams.has("code")) {
87
91
  return Result.fail(ANDCO_ERROR_CODES.INVALID_CALLBACK, {
@@ -95,7 +99,7 @@ export class AndcoOAuth {
95
99
  return await this.#sessionFrom(tokens);
96
100
  }
97
101
  catch (cause) {
98
- return Result.fail(AndcoError.from(cause, "authorization_exchange_failed"));
102
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.AUTHORIZATION_EXCHANGE_FAILED));
99
103
  }
100
104
  }
101
105
  /**
@@ -118,7 +122,7 @@ export class AndcoOAuth {
118
122
  return Result.ok(await this.#resolveAuthorizationUrl(parameters, transport));
119
123
  }
120
124
  catch (cause) {
121
- return Result.fail(AndcoError.from(cause, "authorization_request_failed"));
125
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.AUTHORIZATION_REQUEST_FAILED));
122
126
  }
123
127
  }
124
128
  /** Exchanges a refresh token. The caller decides where the rotated token is written. */
@@ -128,7 +132,7 @@ export class AndcoOAuth {
128
132
  return await this.#sessionFrom(tokens);
129
133
  }
130
134
  catch (cause) {
131
- return Result.fail(AndcoError.from(cause, "refresh_failed"));
135
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.REFRESH_FAILED));
132
136
  }
133
137
  }
134
138
  /** Reads the authenticated subject for one access token. */
@@ -137,25 +141,48 @@ export class AndcoOAuth {
137
141
  // The subject check compares the UserInfo response against the token's own `sub`, which
138
142
  // exists only when the token is a JWT. An opaque token offers nothing to compare, so the
139
143
  // check is skipped rather than failing a request that is otherwise valid.
140
- const subject = decodeSubject(accessToken);
141
- const info = await oidc.fetchUserInfo(this.#oidc(), accessToken, subject ? subject.sub : oidc.skipSubjectCheck);
144
+ const subject = decodeJwtPayload(accessToken)?.["sub"];
145
+ const info = await oidc.fetchUserInfo(this.#oidc(), accessToken, typeof subject === "string" ? subject : oidc.skipSubjectCheck);
142
146
  return Result.ok({
143
147
  id: String(info.sub),
144
- name: info.name,
145
- email: info.email,
146
- avatarUrl: info.picture,
148
+ name: typeof info.name === "string" ? info.name : null,
149
+ email: typeof info.email === "string" ? info.email : null,
150
+ avatarUrl: typeof info.picture === "string" ? info.picture : null,
151
+ emailVerified: typeof info.email_verified === "boolean" ? info.email_verified : null,
152
+ phone: typeof info["phone"] === "string" ? info["phone"] : null,
153
+ phoneVerified: typeof info["phone_verified"] === "boolean" ? info["phone_verified"] : null,
147
154
  });
148
155
  }
149
156
  catch (cause) {
150
- return Result.fail(AndcoError.from(cause, "userinfo_failed"));
157
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.USERINFO_FAILED));
151
158
  }
152
159
  }
153
- /** Starts RFC 8628 Device Authorization for a runtime that cannot present a browser. */
160
+ /**
161
+ * Starts RFC 8628 Device Authorization for a runtime that cannot present a browser.
162
+ *
163
+ * Takes the same permissions as {@link createAuthorizationRequest}: scopes, Resource Server
164
+ * contributions (bound to their server through the same plan, with one `resource` parameter
165
+ * each) and loose `authorizationDetails`. They travel in the device request's form body, which
166
+ * the Authorization Server records exactly as it records a pushed request.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * const { data: device } = await andco.oauth.createDeviceAuthorizationRequest({
171
+ * scopes: ["openid", "email"],
172
+ * authorizations: [bank.accounts.authorization({ actions: ["read"], subject: { type: "profile" } })],
173
+ * });
174
+ * console.log(device.verificationUriComplete, device.userCode);
175
+ * ```
176
+ */
154
177
  async createDeviceAuthorizationRequest(options = {}) {
155
178
  try {
156
- const response = await oidc.initiateDeviceAuthorization(this.#oidc(), {
157
- scope: this.#scopes(options).join(" "),
158
- });
179
+ const parameters = new URLSearchParams();
180
+ const scopes = this.#scopes(options);
181
+ if (scopes.length)
182
+ parameters.set("scope", scopes.join(" "));
183
+ this.#appendPermissions(parameters, options);
184
+ console.debug("[AndcoOAuth.createDeviceAuthorizationRequest] scopes %s resources %o details %s", scopes.join(" "), parameters.getAll("resource"), parameters.has("authorization_details"));
185
+ const response = await oidc.initiateDeviceAuthorization(this.#oidc(), parameters);
159
186
  return Result.ok({
160
187
  deviceCode: response.device_code,
161
188
  userCode: response.user_code,
@@ -167,7 +194,7 @@ export class AndcoOAuth {
167
194
  });
168
195
  }
169
196
  catch (cause) {
170
- return Result.fail(AndcoError.from(cause, "device_authorization_failed"));
197
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.DEVICE_AUTHORIZATION_FAILED));
171
198
  }
172
199
  }
173
200
  /**
@@ -185,7 +212,7 @@ export class AndcoOAuth {
185
212
  return await this.#sessionFrom(tokens);
186
213
  }
187
214
  catch (cause) {
188
- return Result.fail(AndcoError.from(cause, "device_exchange_failed"));
215
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.DEVICE_EXCHANGE_FAILED));
189
216
  }
190
217
  }
191
218
  /**
@@ -226,7 +253,7 @@ export class AndcoOAuth {
226
253
  const configuration = new oidc.Configuration(andcoServerMetadata(issuer), config.clientId, secret ?? undefined, secret ? oidc.ClientSecretBasic(secret) : oidc.None());
227
254
  // `CustomFetch` widens the body to include `Uint8Array`, which every runtime's `fetch` accepts
228
255
  // as a `BufferSource` even though the standard lib types do not spell it that way.
229
- configuration[oidc.customFetch] = ((input, init) => this.#fetch(input, init));
256
+ configuration[oidc.customFetch] = ((input, init) => this.#globals.fetch(input, init));
230
257
  // The Andco Authorization Server is reachable over loopback HTTP in local development.
231
258
  oidc.allowInsecureRequests(configuration);
232
259
  return configuration;
@@ -245,6 +272,30 @@ export class AndcoOAuth {
245
272
  const requested = options.scopes ?? this.#config.initialScopes;
246
273
  return [...new Set([...requested, ...contributed])];
247
274
  }
275
+ /**
276
+ * Appends the `resource` parameters and `authorization_details` both the browser and the device
277
+ * request carry. Each Resource Server contribution goes through the protocol's plan, which binds
278
+ * its details to that server (`locations: [resource]`) and refuses details naming different
279
+ * subjects, loose `authorizationDetails` included. A detail sent without `locations` does not match the server's registered schema and
280
+ * Andco cannot record it.
281
+ */
282
+ #appendPermissions(parameters, options) {
283
+ const resources = new Set();
284
+ for (const value of toArray(options.resource))
285
+ resources.add(value);
286
+ for (const contribution of options.authorizations ?? [])
287
+ resources.add(contribution.resource);
288
+ for (const resource of resources)
289
+ parameters.append("resource", resource);
290
+ // Loose details join the plan as a contribution without a resource: they keep their own
291
+ // `locations`, and their subjects are compared with every contribution's.
292
+ const plan = createAndCoAuthorizationPlan([
293
+ { authorizationDetails: options.authorizationDetails ?? [] },
294
+ ...(options.authorizations ?? []),
295
+ ]);
296
+ if (plan.authorizationDetails.length)
297
+ parameters.set("authorization_details", JSON.stringify(plan.authorizationDetails));
298
+ }
248
299
  #authorizationParameters(options, redirectTo, state, codeChallenge) {
249
300
  const parameters = new URLSearchParams({
250
301
  client_id: this.#config.clientId,
@@ -257,22 +308,7 @@ export class AndcoOAuth {
257
308
  const scopes = this.#scopes(options);
258
309
  if (scopes.length)
259
310
  parameters.set("scope", scopes.join(" "));
260
- const resources = new Set();
261
- for (const value of toArray(options.resource))
262
- resources.add(value);
263
- for (const contribution of options.authorizations ?? [])
264
- resources.add(contribution.resource);
265
- for (const resource of resources)
266
- parameters.append("resource", resource);
267
- const details = [
268
- ...(options.authorizationDetails ?? []),
269
- ...(options.authorizations ?? []).flatMap((a) => [...(a.authorizationDetails ?? [])]),
270
- ];
271
- if (details.length)
272
- parameters.set("authorization_details", JSON.stringify(details));
273
- const orgId = options.orgId ?? (options.authorizations ?? []).find((a) => a.orgId)?.orgId;
274
- if (orgId)
275
- parameters.set("org_id", orgId);
311
+ this.#appendPermissions(parameters, options);
276
312
  if (options.externalId)
277
313
  parameters.set("external_id", options.externalId);
278
314
  return parameters;
@@ -299,21 +335,30 @@ export class AndcoOAuth {
299
335
  }
300
336
  async #sessionFrom(tokens) {
301
337
  if (tokens.token_type && tokens.token_type.toLowerCase() !== "bearer") {
302
- return Result.fail(ANDCO_ERROR_CODES.INVALID_RESPONSE, { message: "unsupported token type" });
338
+ return Result.fail(ANDCO_ERROR_CODES.INVALID_TOKEN_RESPONSE, { message: "unsupported token type" });
303
339
  }
304
340
  // The ID Token is where OpenID Connect puts the subject. Reading it out of the access token
305
341
  // works only while that token happens to be a JWT, which no specification promises and an
306
342
  // Authorization Server may stop doing without warning.
307
- const subject = tokens.claims?.()?.sub ?? decodeSubject(tokens.access_token)?.sub;
343
+ const accessSubject = decodeJwtPayload(tokens.access_token)?.["sub"];
344
+ const subject = tokens.claims?.()?.sub ?? (typeof accessSubject === "string" ? accessSubject : undefined);
308
345
  if (!subject)
309
- return Result.fail(ANDCO_ERROR_CODES.INVALID_RESPONSE, { message: "the response carries no subject" });
346
+ return Result.fail(ANDCO_ERROR_CODES.INVALID_TOKEN_RESPONSE, { message: "the response carries no subject" });
310
347
  const session = {
311
348
  accessToken: tokens.access_token,
312
349
  refreshToken: tokens.refresh_token ?? null,
313
350
  tokenType: tokens.token_type ?? "bearer",
314
351
  expiresAt: Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 0),
315
352
  scopes: (tokens.scope ?? "").split(/\s+/).filter(Boolean),
316
- user: { id: subject, name: null, email: null, avatarUrl: null },
353
+ user: {
354
+ id: subject,
355
+ name: null,
356
+ email: null,
357
+ avatarUrl: null,
358
+ emailVerified: null,
359
+ phone: null,
360
+ phoneVerified: null,
361
+ },
317
362
  };
318
363
  const user = await this.userInfo(session.accessToken);
319
364
  return Result.ok(user.data ? { ...session, user: user.data } : session);
@@ -330,19 +375,3 @@ function callbackMatchesRedirect(actual, expected) {
330
375
  return false;
331
376
  return [...expected.searchParams].every(([key, value]) => actual.searchParams.getAll(key).includes(value));
332
377
  }
333
- /** Reads `sub` from an access token without verifying it; the server remains the authority. */
334
- function decodeSubject(accessToken) {
335
- const segments = accessToken.split(".");
336
- if (segments.length < 2 || !segments[1])
337
- return null;
338
- try {
339
- const payload = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(segments[1].replace(/-/g, "+").replace(/_/g, "/")), (character) => character.charCodeAt(0))));
340
- if (payload && typeof payload === "object" && typeof payload.sub === "string") {
341
- return { sub: payload.sub };
342
- }
343
- return null;
344
- }
345
- catch {
346
- return null;
347
- }
348
- }
@@ -1,9 +1,23 @@
1
- import type { Result } from "./errors.js";
1
+ import { Result } from "./errors.js";
2
2
  /** How an authorization is put in front of the user. */
3
3
  export type AndcoPresentation = "popup" | "redirect";
4
- export type AndcoPresentOptions = {
5
- url: URL;
6
- presentation: AndcoPresentation;
4
+ /**
5
+ * How an Intent is put in front of the user. `"newtab"` is a popup without window features: same
6
+ * relay back to the opener, for where popups are blocked.
7
+ */
8
+ export type AndcoIntentPresentation = "popup" | "newtab" | "redirect";
9
+ /**
10
+ * The destination, or a thunk that resolves it.
11
+ *
12
+ * A thunk is what lets a presenter open its window *during* the user activation and navigate it
13
+ * afterwards. Both flows need it: an authorization request may be pushed (PAR) before its URL
14
+ * exists, and an Intent is created only once someone asked for it. Awaiting either before opening
15
+ * is what gets the window blocked. A thunk that cannot resolve throws an {@link AndcoError} rather
16
+ * than returning one; {@link AndcoPresenter.present} is what turns that back into a `Result` for a presenter.
17
+ */
18
+ export type AndcoPresentTarget = URL | (() => Promise<URL>);
19
+ type AndcoPresentBase = {
20
+ url: AndcoPresentTarget;
7
21
  /** Exact callback the Authorization Server will reach on success. */
8
22
  returnTo: URL;
9
23
  /** Exact callback reached on failure. Defaults to `returnTo`. */
@@ -12,11 +26,34 @@ export type AndcoPresentOptions = {
12
26
  presentationId: string;
13
27
  signal?: AbortSignal;
14
28
  };
29
+ /** An authorization. Its callback carries a single-use code the opener must exchange. */
30
+ export type AndcoPresentOAuth = AndcoPresentBase & {
31
+ kind: "oauth";
32
+ presentation: AndcoPresentation;
33
+ };
34
+ /**
35
+ * An Intent. Its callback carries a lifecycle outcome only — the authenticated read decides the
36
+ * financial fact — and `intentId` is what lets a presenter with no window, such as a native Host,
37
+ * resolve current state from the Resource Server instead.
38
+ */
39
+ export type AndcoPresentIntent = AndcoPresentBase & {
40
+ kind: "intent";
41
+ /** A `"redirect"` that navigates answers `null`; its result is read by `andco.intents.fromCallback`. */
42
+ presentation: AndcoIntentPresentation;
43
+ /** Empty until the thunk has resolved; a presenter reads it after resolution, never before. */
44
+ readonly intentId: string;
45
+ };
46
+ export type AndcoPresentOptions = AndcoPresentOAuth | AndcoPresentIntent;
15
47
  /**
16
48
  * The boundary between the protocol and whatever shows it to a person.
17
49
  *
18
- * A presenter receives an authorization URL and answers with the callback URL that came back, or
19
- * `null` when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange.
50
+ * A presenter receives a destination and answers with the callback URL that came back, or `null`
51
+ * when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange, and no
52
+ * opinion about what a completed Intent means.
53
+ *
54
+ * `kind` is the seam's only domain knowledge, and it exists because a presenter without a window
55
+ * cannot treat the two alike: a native Host runs its own consent screen for an authorization and
56
+ * its own review screen for an Intent. A browser presents both the same way and ignores it.
20
57
  *
21
58
  * Keeping it this narrow is what makes sign-in testable without a browser — a test supplies a
22
59
  * presenter that returns a prepared callback URL and the whole flow runs in plain Node. It is also
@@ -25,12 +62,23 @@ export type AndcoPresentOptions = {
25
62
  *
26
63
  * @example
27
64
  * ```ts
28
- * const presenter: AndcoPresenter = {
29
- * present: async ({ url }) => Result.ok(new URL(`${redirectTo}?code=test&state=${state}`)),
30
- * };
65
+ * class Presenter extends AndcoPresenter {
66
+ * async present({ returnTo }: AndcoPresentOptions) {
67
+ * return Result.ok(new URL(`${returnTo}?code=test&state=${state}`));
68
+ * }
69
+ * }
31
70
  * ```
32
71
  */
33
- export interface AndcoPresenter {
34
- present(options: AndcoPresentOptions): Promise<Result<URL | null>>;
72
+ export declare abstract class AndcoPresenter {
73
+ /** Resolves a presentation target, whether it was already a URL or still a thunk. */
74
+ static present(target: AndcoPresentTarget): Promise<Result<URL>>;
75
+ abstract present(options: AndcoPresentOptions): Promise<Result<URL | null>>;
76
+ /**
77
+ * Brings the window of a presentation that is still open to the front, so presenting the same
78
+ * Intent twice shows the one window instead of opening a second. A presenter with no window of
79
+ * its own, such as a native Host that owns its screen, simply omits it.
80
+ */
81
+ focus?(presentationId: string): void;
35
82
  }
83
+ export {};
36
84
  //# sourceMappingURL=presenter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,CAAC;AAErD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,iBAAiB,CAAC;IAChC,qEAAqE;IACrE,QAAQ,EAAE,GAAG,CAAC;IACd,iEAAiE;IACjE,aAAa,CAAC,EAAE,GAAG,CAAC;IACpB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;CACpE"}
1
+ {"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,MAAM,EAAE,MAAM,aAAa,CAAC;AAEpE,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,CAAC;AAErD;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEtE;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,GAAG,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AAE5D,KAAK,gBAAgB,GAAG;IACtB,GAAG,EAAE,kBAAkB,CAAC;IACxB,qEAAqE;IACrE,QAAQ,EAAE,GAAG,CAAC;IACd,iEAAiE;IACjE,aAAa,CAAC,EAAE,GAAG,CAAC;IACpB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,iBAAiB,CAAC;CACjC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAClD,IAAI,EAAE,QAAQ,CAAC;IACf,wGAAwG;IACxG,YAAY,EAAE,uBAAuB,CAAC;IACtC,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG,kBAAkB,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8BAAsB,cAAc;IAClC,qFAAqF;WACxE,OAAO,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAWtE,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAE3E;;;;OAIG;IACH,KAAK,CAAC,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;CACrC"}
package/dist/presenter.js CHANGED
@@ -1 +1,40 @@
1
- export {};
1
+ import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
2
+ /**
3
+ * The boundary between the protocol and whatever shows it to a person.
4
+ *
5
+ * A presenter receives a destination and answers with the callback URL that came back, or `null`
6
+ * when the user dismissed. It owns no protocol: no PKCE, no state, no token exchange, and no
7
+ * opinion about what a completed Intent means.
8
+ *
9
+ * `kind` is the seam's only domain knowledge, and it exists because a presenter without a window
10
+ * cannot treat the two alike: a native Host runs its own consent screen for an authorization and
11
+ * its own review screen for an Intent. A browser presents both the same way and ignores it.
12
+ *
13
+ * Keeping it this narrow is what makes sign-in testable without a browser — a test supplies a
14
+ * presenter that returns a prepared callback URL and the whole flow runs in plain Node. It is also
15
+ * why a redirect presentation can answer with `null` and never resolve: the document is replaced,
16
+ * and the result arrives on the next page load instead.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * class Presenter extends AndcoPresenter {
21
+ * async present({ returnTo }: AndcoPresentOptions) {
22
+ * return Result.ok(new URL(`${returnTo}?code=test&state=${state}`));
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export class AndcoPresenter {
28
+ /** Resolves a presentation target, whether it was already a URL or still a thunk. */
29
+ static async present(target) {
30
+ if (target instanceof URL) {
31
+ return Result.ok(target);
32
+ }
33
+ try {
34
+ return Result.ok(await target());
35
+ }
36
+ catch (cause) {
37
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.PRESENTATION_TARGET_FAILED));
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,109 @@
1
+ import { Result } from "./errors.js";
2
+ import type { AndcoGrantAuthorization, AndcoGrants } from "./grants-api.js";
3
+ import type { AndcoTokens } from "./tokens.js";
4
+ /** One RAR detail a route needs the Grant to carry: the type, and every action it uses. */
5
+ export type AndcoDetailRequirement = {
6
+ readonly type: string;
7
+ readonly actions?: readonly string[];
8
+ };
9
+ /** What a protected route requires of the caller's Grant. Everything omitted is not checked. */
10
+ export type AndcoProtectOptions = {
11
+ /** Scopes the Grant must carry, all of them. */
12
+ scopes?: readonly string[];
13
+ /** RAR details the Grant must carry; each needs a detail of that type including its actions. */
14
+ authorizationDetails?: readonly AndcoDetailRequirement[];
15
+ /**
16
+ * Verify the token offline first (signature, `exp`, `iss`, and `aud` equal to the resource), so a
17
+ * forged token never costs a request to Andco. Off by default: it assumes the Authorization Server
18
+ * addresses tokens to this resource.
19
+ */
20
+ verify?: boolean;
21
+ };
22
+ /** A guard answered `Response` to reject the request, or the live Grant to let it through. */
23
+ export type AndcoProtectOutcome = Result<AndcoGrantAuthorization> | Response;
24
+ /** Hono's `Context`, as far as the adapter needs it. */
25
+ type HonoLikeContext = {
26
+ req: {
27
+ raw: Request;
28
+ };
29
+ set(key: "andcoGrant", value: AndcoGrantAuthorization): void;
30
+ };
31
+ /** Express's request, as far as the adapter needs it. */
32
+ type ExpressLikeRequest = {
33
+ headers: Record<string, string | string[] | undefined>;
34
+ };
35
+ /** Express's response, as far as the adapter needs it. */
36
+ type ExpressLikeResponse = {
37
+ locals: Record<string, unknown>;
38
+ status(code: number): unknown;
39
+ setHeader(name: string, value: string): unknown;
40
+ send(body: string): unknown;
41
+ };
42
+ /**
43
+ * A route guard built by {@link AndcoResource.protect}. Call it with a Fetch `Request`, or plug one
44
+ * of its adapters into a framework.
45
+ */
46
+ export type AndcoProtection = {
47
+ (request: Request): Promise<AndcoProtectOutcome>;
48
+ /** Hono middleware. The Grant is available as `c.get("andcoGrant")`. */
49
+ hono(): (context: HonoLikeContext, next: () => Promise<void>) => Promise<Response | undefined>;
50
+ /** Express middleware. The Grant is available as `res.locals.andcoGrant`. */
51
+ express(): (request: ExpressLikeRequest, response: ExpressLikeResponse, next: () => void) => Promise<void>;
52
+ };
53
+ /**
54
+ * One Resource Server's view of Andco: verify the bearer token, fetch the live Grant, and answer
55
+ * 401 or 403 the way RFC 6750 and RFC 9728 describe.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const orders = andco.resource({ resource: "https://casa-norte.example/api" });
60
+ *
61
+ * // Fetch-style handler (Next route handlers, Workers, Bun, Deno)
62
+ * export async function GET(request: Request) {
63
+ * const outcome = await orders.protect({ scopes: ["orders:read"] })(request);
64
+ * if (outcome instanceof Response) return outcome; // 401 or 403, already built
65
+ * return Response.json(await listOrders(outcome.data.subject));
66
+ * }
67
+ *
68
+ * // Hono
69
+ * app.use("/api/orders/*", orders.protect({ authorizationDetails: [{ type: "orders", actions: ["read"] }] }).hono());
70
+ *
71
+ * // Express
72
+ * router.get("/api/orders", orders.protect({ scopes: ["orders:read"] }).express(), handler);
73
+ * ```
74
+ */
75
+ export declare class AndcoResource {
76
+ #private;
77
+ readonly resource: string;
78
+ /** Where `WWW-Authenticate` points clients for this resource's metadata (RFC 9728). */
79
+ readonly resourceMetadata: string;
80
+ constructor(options: {
81
+ resource: string;
82
+ /** Overrides the RFC 9728 well-known URL derived from `resource`. */
83
+ resourceMetadata?: string;
84
+ tokens: AndcoTokens;
85
+ grants: AndcoGrants;
86
+ });
87
+ /**
88
+ * The document to publish at {@link resourceMetadata}, so clients can discover the Authorization Server.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * app.get("/.well-known/oauth-protected-resource/api", (_req, res) =>
93
+ * res.json(orders.metadata({ scopesSupported: ["orders:read"] })),
94
+ * );
95
+ * ```
96
+ */
97
+ metadata(options?: {
98
+ scopesSupported?: readonly string[];
99
+ }): {
100
+ resource: string;
101
+ authorization_servers: string[];
102
+ bearer_methods_supported: string[];
103
+ scopes_supported: readonly string[] | undefined;
104
+ };
105
+ /** Builds a guard requiring a live Grant for this resource that satisfies `options`. */
106
+ protect(options?: AndcoProtectOptions): AndcoProtection;
107
+ }
108
+ export {};
109
+ //# sourceMappingURL=resource.d.ts.map