@shuttle.technology/authorization 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +1082 -666
  3. package/dist/accountLists.d.ts +28 -0
  4. package/dist/accountLists.js +27 -0
  5. package/dist/auth.d.ts +11 -11
  6. package/dist/auth.js +28 -54
  7. package/dist/client.d.ts +62 -174
  8. package/dist/client.js +381 -354
  9. package/dist/clientTypes.d.ts +56 -0
  10. package/dist/clientTypes.js +5 -0
  11. package/dist/configuration.d.ts +48 -0
  12. package/dist/configuration.js +72 -0
  13. package/dist/consentDebit.d.ts +36 -0
  14. package/dist/consentDebit.js +95 -0
  15. package/dist/credentials.d.ts +116 -0
  16. package/dist/credentials.js +315 -0
  17. package/dist/debitPermissionWire.d.ts +90 -0
  18. package/dist/debitPermissionWire.js +118 -0
  19. package/dist/debitPermissions.d.ts +39 -0
  20. package/dist/debitPermissions.js +117 -0
  21. package/dist/deviceRecovery.d.ts +40 -0
  22. package/dist/deviceRecovery.js +143 -0
  23. package/dist/deviceShares.d.ts +91 -0
  24. package/dist/deviceShares.js +204 -0
  25. package/dist/errors.d.ts +280 -0
  26. package/dist/errors.js +85 -0
  27. package/dist/linkedRecovery.d.ts +40 -0
  28. package/dist/linkedRecovery.js +49 -0
  29. package/dist/linkedRegistrations.d.ts +11 -0
  30. package/dist/linkedRegistrations.js +109 -0
  31. package/dist/linkedWallets.d.ts +46 -18
  32. package/dist/linkedWallets.js +85 -120
  33. package/dist/me.d.ts +27 -0
  34. package/dist/me.js +30 -0
  35. package/dist/mobile.d.ts +64 -0
  36. package/dist/mobile.js +69 -0
  37. package/dist/redirects.d.ts +45 -0
  38. package/dist/redirects.js +108 -0
  39. package/dist/serviceGrants.d.ts +52 -0
  40. package/dist/serviceGrants.js +46 -0
  41. package/dist/slice4.d.ts +20 -6
  42. package/dist/slice4.js +22 -8
  43. package/dist/streams.js +1 -1
  44. package/dist/walletCreateGrant.d.ts +62 -0
  45. package/dist/walletCreateGrant.js +89 -0
  46. package/dist/walletDisplayParts.d.ts +2 -0
  47. package/dist/walletDisplayParts.js +4 -0
  48. package/dist/walletOperations.d.ts +23 -6
  49. package/dist/walletOperations.js +31 -22
  50. package/dist/webRenewal.d.ts +92 -0
  51. package/dist/webRenewal.js +179 -0
  52. package/package.json +25 -23
@@ -0,0 +1,45 @@
1
+ import { type AuthIntent } from "./auth.ts";
2
+ import type { Session } from "./client.ts";
3
+ /**
4
+ * HostedSignIn is a sign-in started on the provider's hosted page. The person
5
+ * picks the method there. On the web the state and the S256 verifier stay in
6
+ * this client's memory until `exchangeHandoff` receives the redirect back; on
7
+ * mobile the system browser hands the redirect back to the same call, which
8
+ * exchanges it and answers the session.
9
+ */
10
+ export interface HostedSignIn {
11
+ intent: AuthIntent;
12
+ launchUrl: string;
13
+ session?: Session;
14
+ }
15
+ /** hostedSignInUrl is the provider's sign-in page for one client, intent, state and S256 challenge. */
16
+ export declare function hostedSignInUrl(authorizationOrigin: string, input: {
17
+ clientId: string;
18
+ redirectUri: string;
19
+ state: string;
20
+ challenge: string;
21
+ intent: AuthIntent;
22
+ }): string;
23
+ /**
24
+ * handoffCallback reads the redirect back from a sign-in at the exact
25
+ * registered redirect. A sign-in begun on the hosted page returns the code and
26
+ * the state alone; one begun through the attempt API also names its attempt.
27
+ */
28
+ export declare function handoffCallback(value: string, expected: string): {
29
+ attemptId?: string;
30
+ state: string;
31
+ code: string;
32
+ };
33
+ /** restoreCallback reads the redirect back from a web restore at the exact registered redirect. */
34
+ export declare function restoreCallback(value: string, expected: string): {
35
+ restoreId: string;
36
+ state: string;
37
+ code: string;
38
+ };
39
+ /** The app's own exact origin: https on any host, or http on a loopback host. */
40
+ export declare function exactAppOrigin(value: string): boolean;
41
+ /** A redirect back to the app: https on any host, or http on a loopback host; no credentials, query or fragment. */
42
+ export declare function exactAppRedirect(value: string): boolean;
43
+ export declare function exactHttpsOrigin(value: string): boolean;
44
+ export declare function exactHttpsRedirect(value: string): boolean;
45
+ export declare function httpsURL(value: unknown): value is string;
@@ -0,0 +1,108 @@
1
+ // The provider's top-level sign-in page, the redirects that come back to the
2
+ // application from a sign-in or a restore, and the exact HTTPS shapes the
3
+ // configuration and the provider's answers are checked against.
4
+ import { ClientFlowError, opaqueId } from "./auth.js";
5
+ /** hostedSignInUrl is the provider's sign-in page for one client, intent, state and S256 challenge. */
6
+ export function hostedSignInUrl(authorizationOrigin, input) {
7
+ const start = new URL("/v1/auth/start", authorizationOrigin);
8
+ start.searchParams.set("client_id", input.clientId);
9
+ start.searchParams.set("redirect_uri", input.redirectUri);
10
+ start.searchParams.set("state", input.state);
11
+ start.searchParams.set("code_challenge", input.challenge);
12
+ start.searchParams.set("code_challenge_method", "S256");
13
+ start.searchParams.set("intent", input.intent);
14
+ return start.toString();
15
+ }
16
+ /**
17
+ * handoffCallback reads the redirect back from a sign-in at the exact
18
+ * registered redirect. A sign-in begun on the hosted page returns the code and
19
+ * the state alone; one begun through the attempt API also names its attempt.
20
+ */
21
+ export function handoffCallback(value, expected) {
22
+ const callback = exactRedirect(value, expected);
23
+ const code = callback.searchParams.getAll("code");
24
+ const state = callback.searchParams.getAll("state");
25
+ const attemptId = callback.searchParams.getAll("attempt_id");
26
+ const size = callback.searchParams.size;
27
+ if (code.length !== 1 || state.length !== 1 || code[0] === "" || state[0] === "" || !((size === 2 && attemptId.length === 0) || (size === 3 && attemptId.length === 1 && opaqueId(attemptId[0])))) {
28
+ throw new ClientFlowError("invalid_callback");
29
+ }
30
+ return attemptId.length === 1 ? { code: code[0], state: state[0], attemptId: attemptId[0] } : { code: code[0], state: state[0] };
31
+ }
32
+ /** restoreCallback reads the redirect back from a web restore at the exact registered redirect. */
33
+ export function restoreCallback(value, expected) {
34
+ const callback = exactRedirect(value, expected);
35
+ const code = callback.searchParams.getAll("code");
36
+ const state = callback.searchParams.getAll("state");
37
+ const restoreId = callback.searchParams.getAll("restore_id");
38
+ if (callback.searchParams.size !== 3 || code.length !== 1 || state.length !== 1 || restoreId.length !== 1 || code[0] === "" || state[0] === "" || !opaqueId(restoreId[0]))
39
+ throw new ClientFlowError("invalid_callback");
40
+ return { code: code[0], state: state[0], restoreId: restoreId[0] };
41
+ }
42
+ function exactRedirect(value, expected) {
43
+ let callback;
44
+ try {
45
+ callback = new URL(value);
46
+ }
47
+ catch {
48
+ throw new ClientFlowError("invalid_callback");
49
+ }
50
+ const target = new URL(expected);
51
+ if (`${callback.origin}${callback.pathname}` !== `${target.origin}${target.pathname}` || callback.hash !== "")
52
+ throw new ClientFlowError("invalid_callback");
53
+ return callback;
54
+ }
55
+ /** A loopback host a browser treats as a secure context over plain http (local development). */
56
+ function loopbackHost(hostname) {
57
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
58
+ }
59
+ /** The app's own exact origin: https on any host, or http on a loopback host. */
60
+ export function exactAppOrigin(value) {
61
+ try {
62
+ const parsed = new URL(value);
63
+ const secure = parsed.protocol === "https:" || (parsed.protocol === "http:" && loopbackHost(parsed.hostname));
64
+ return secure && parsed.username === "" && parsed.password === "" && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "" && parsed.origin === value;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /** A redirect back to the app: https on any host, or http on a loopback host; no credentials, query or fragment. */
71
+ export function exactAppRedirect(value) {
72
+ try {
73
+ const parsed = new URL(value);
74
+ const secure = parsed.protocol === "https:" || (parsed.protocol === "http:" && loopbackHost(parsed.hostname));
75
+ return secure && parsed.username === "" && parsed.password === "" && parsed.search === "" && parsed.hash === "";
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ export function exactHttpsOrigin(value) {
82
+ try {
83
+ const parsed = new URL(value);
84
+ return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "" && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "" && parsed.origin === value;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ export function exactHttpsRedirect(value) {
91
+ try {
92
+ const parsed = new URL(value);
93
+ return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "" && parsed.search === "" && parsed.hash === "";
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ export function httpsURL(value) {
100
+ if (typeof value !== "string")
101
+ return false;
102
+ try {
103
+ return new URL(value).protocol === "https:";
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
@@ -0,0 +1,52 @@
1
+ import type { LocalOperationCreate, ServiceReadScope } from "./slice4.ts";
2
+ /** What a grant gives, or changes a grant to; `enabled: false` withdraws it. */
3
+ export interface ServiceGrantInput {
4
+ walletId: string;
5
+ servicePrincipalId: string;
6
+ enabled: boolean;
7
+ scopes: ServiceReadScope[];
8
+ /** The service's own reference for its user, returned only to that service; it proves nothing. */
9
+ externalRef?: string;
10
+ }
11
+ /** The one grant to withdraw: the wallet and the service it names. */
12
+ export interface ServiceGrantTarget {
13
+ walletId: string;
14
+ servicePrincipalId: string;
15
+ }
16
+ /**
17
+ * pending_source: a first streams:read grant waits until Streams opens the
18
+ * wallet's events to the service. active: the service reads under it now.
19
+ * revoked: withdrawn with `enabled: false`. stale: the wallet or the service
20
+ * changed since the grant was set, and the service reads nothing under it
21
+ * until it is set again.
22
+ */
23
+ export type ServiceGrantState = "pending_source" | "active" | "revoked" | "stale";
24
+ /**
25
+ * One grant the account gave. walletKind names the wallet as me() does: "wallet" for a wallet Authorization
26
+ * created for the account, "linked_wallet" for one of its linked wallets. network and address are empty for a
27
+ * wallet that is no longer the one granted.
28
+ */
29
+ export interface AccountServiceGrant {
30
+ servicePrincipalId: string;
31
+ walletId: string;
32
+ walletKind: "wallet" | "linked_wallet";
33
+ network: string;
34
+ address: string;
35
+ scopes: ServiceReadScope[];
36
+ state: ServiceGrantState;
37
+ /** UTC RFC 3339 date-time of the last change. */
38
+ grantedAt: string;
39
+ externalRef?: string;
40
+ }
41
+ export interface ServiceGrantPage {
42
+ requestId: string;
43
+ items: AccountServiceGrant[];
44
+ hasMore: boolean;
45
+ nextCursor?: string;
46
+ }
47
+ /** serviceGrantCreate is the closed `service_grant.set` body of the account. */
48
+ export declare function serviceGrantCreate(ownerId: string, input: ServiceGrantInput): LocalOperationCreate;
49
+ /** serviceGrantRevocation withdraws the grant of one service on one wallet: enabled false, no scopes. */
50
+ export declare function serviceGrantRevocation(ownerId: string, target: ServiceGrantTarget): LocalOperationCreate;
51
+ /** parseServiceGrantPage reads exactly one `GET /v1/service-grants` page. */
52
+ export declare function parseServiceGrantPage(payload: unknown): ServiceGrantPage;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The grants an account gives services on its wallets. `service_grant.set`
3
+ * gives one service read access to one wallet the account owns, changes it or
4
+ * withdraws it (`enabled: false`); the person confirms every change with a
5
+ * passkey. `GET /v1/service-grants` lists the grants the account gave, in every
6
+ * state, so the application can show them and offer to withdraw one.
7
+ */
8
+ import { ClientFlowError, opaqueId, record, rfc3339UTC } from "./auth.js";
9
+ import { pageEnvelope, pageProjection } from "./pages.js";
10
+ const readScopes = ["wallet:read", "operation:read", "streams:read"];
11
+ /** serviceGrantCreate is the closed `service_grant.set` body of the account. */
12
+ export function serviceGrantCreate(ownerId, input) {
13
+ const payload = { service_principal_id: input.servicePrincipalId, enabled: input.enabled, scopes: [...input.scopes] };
14
+ if (input.externalRef !== undefined)
15
+ payload.external_ref = input.externalRef;
16
+ return { kind: "service_grant.set", owner_type: "account", owner_id: ownerId, wallet_id: input.walletId, payload };
17
+ }
18
+ /** serviceGrantRevocation withdraws the grant of one service on one wallet: enabled false, no scopes. */
19
+ export function serviceGrantRevocation(ownerId, target) {
20
+ if (target === null || typeof target !== "object")
21
+ throw new ClientFlowError("invalid_operation_create");
22
+ return serviceGrantCreate(ownerId, { walletId: target.walletId, servicePrincipalId: target.servicePrincipalId, enabled: false, scopes: [] });
23
+ }
24
+ /** parseServiceGrantPage reads exactly one `GET /v1/service-grants` page. */
25
+ export function parseServiceGrantPage(payload) {
26
+ const root = pageEnvelope(payload);
27
+ const items = root.items.map((value) => {
28
+ const item = record(value, ["address", "granted_at", "network", "scopes", "service_principal_id", "state", "wallet_id", "wallet_kind"], ["external_ref"]);
29
+ const scopes = item.scopes;
30
+ if (!opaqueId(item.service_principal_id) || !opaqueId(item.wallet_id) || (item.wallet_kind !== "wallet" && item.wallet_kind !== "linked_wallet") ||
31
+ typeof item.network !== "string" || item.network.length > 64 || typeof item.address !== "string" || item.address.length > 256 ||
32
+ !Array.isArray(scopes) || scopes.length > readScopes.length || new Set(scopes).size !== scopes.length || !scopes.every((scope) => readScopes.includes(scope)) ||
33
+ (item.state !== "pending_source" && item.state !== "active" && item.state !== "revoked" && item.state !== "stale") || !rfc3339UTC(item.granted_at) ||
34
+ (item.external_ref !== undefined && (typeof item.external_ref !== "string" || !/^[\x20-\x7E]{1,128}$/.test(item.external_ref)))) {
35
+ throw new ClientFlowError("invalid_server_response");
36
+ }
37
+ const grant = {
38
+ servicePrincipalId: item.service_principal_id, walletId: item.wallet_id, walletKind: item.wallet_kind, network: item.network, address: item.address,
39
+ scopes: [...scopes], state: item.state, grantedAt: item.granted_at,
40
+ };
41
+ if (item.external_ref !== undefined)
42
+ grant.externalRef = item.external_ref;
43
+ return grant;
44
+ });
45
+ return pageProjection(root.request_id, items, root.has_more, root.next_cursor);
46
+ }
package/dist/slice4.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type IdentityMethod, type NextAction } from "./auth.ts";
2
2
  import { type WalletLimitsDisplay } from "./walletLimits.ts";
3
+ import { type DebitPermissionDisplay, type DebitPermissionRevokeDisplay } from "./debitPermissionWire.ts";
4
+ import { type ConsentDebitOutcome, type DebitTerms } from "./consentDebit.ts";
3
5
  import { type LinkedEnrollmentDisplay, type LinkedRequestGrantEnrollmentCreate, type LinkedRequestGrantResult } from "./modeCLinked.ts";
4
6
  export interface HostedIdentityFlow {
5
7
  requestId: string;
@@ -35,10 +37,11 @@ export declare function parseIdentityList(payload: unknown): AccountIdentity[];
35
37
  /**
36
38
  * wallet_limits.set changes a linked wallet's limits (LinkedWallets.setLimits); its result kind is wallet_limits.
37
39
  * session.authorize_requests enrolls a Mode C request key on a linked wallet (modeCLinked.ts); its result kind is
38
- * request_grant, with the grant.
40
+ * request_grant, with the grant. debit_permission.create and debit_permission.revoke give and end a service's
41
+ * permission to debit a linked wallet without the device (debitPermissions.ts); their result kind is debit_permission.
39
42
  */
40
- export type LocalOperationKind = "identity.link" | "identity.unlink" | "session.revoke" | "service_grant.set" | "wallet_limits.set" | "session.authorize_requests";
41
- export type LocalResultKind = "identity" | "session" | "service_grant" | "wallet_limits" | "request_grant";
43
+ export type LocalOperationKind = "identity.link" | "identity.unlink" | "session.revoke" | "service_grant.set" | "wallet_limits.set" | "session.authorize_requests" | "debit_permission.create" | "debit_permission.revoke";
44
+ export type LocalResultKind = "identity" | "session" | "service_grant" | "wallet_limits" | "request_grant" | "debit_permission";
42
45
  export type LocalResultState = "active" | "revoked";
43
46
  export type ServiceReadScope = "wallet:read" | "operation:read" | "streams:read";
44
47
  export type ResultRecipient = {
@@ -99,12 +102,13 @@ export type OperationDisplay = {
99
102
  enabled: boolean;
100
103
  scopes: ServiceReadScope[];
101
104
  externalRef?: string;
102
- } | WalletLimitsDisplay | LinkedEnrollmentDisplay;
105
+ debit?: DebitTerms;
106
+ } | WalletLimitsDisplay | LinkedEnrollmentDisplay | DebitPermissionDisplay | DebitPermissionRevokeDisplay;
103
107
  export interface LocalOperation {
104
108
  id: string;
105
109
  version: string;
106
110
  kind: LocalOperationKind;
107
- state: "awaiting_confirmation" | "authorized" | "succeeded" | "denied" | "canceled" | "expired";
111
+ state: "awaiting_confirmation" | "authorized" | "pending_target_fence" | "succeeded" | "failed" | "denied" | "canceled" | "expired";
108
112
  effectState: "no_effect" | "known";
109
113
  authorizationBasis: "" | "user_confirmed";
110
114
  ownerType: "account";
@@ -138,16 +142,22 @@ export interface ConfirmationChallenge {
138
142
  confirmationLocation: string;
139
143
  /** UTC RFC 3339 date-time string (source §2.2), not NumericDate seconds. */
140
144
  expiresAt: string;
145
+ /** The registered redirect the host's launcher answered once the page came back, when it answered one; complete…Confirmation() takes it. */
146
+ callbackUri?: string;
141
147
  }
142
148
  export interface LocalOperationResult {
143
149
  requestId: string;
144
- /** grant is present for the request_grant kind of a linked wallet's enrollment. */
150
+ /**
151
+ * grant is present for the request_grant kind of a linked wallet's enrollment; debit for a service_grant whose service
152
+ * holds wallet:debit:user: the debit permission the grant's passkey also created (consentDebit.ts).
153
+ */
145
154
  result: {
146
155
  kind: LocalResultKind;
147
156
  targetId: string;
148
157
  newVersion: string;
149
158
  state: LocalResultState;
150
159
  grant?: LinkedRequestGrantResult;
160
+ debit?: ConsentDebitOutcome;
151
161
  };
152
162
  }
153
163
  export declare function parseHostedIdentityFlow(payload: unknown, now: number): HostedIdentityFlow;
@@ -170,4 +180,8 @@ export declare function parseOperationConfirmationCallback(value: string, expect
170
180
  operationId: string;
171
181
  };
172
182
  export declare function parseLocalOperationResult(payload: unknown): LocalOperationResult;
183
+ /** serviceScopes accepts the read scopes of a service grant: at most the three, each once (none: a withdrawal). */
184
+ export declare function serviceScopes(value: unknown): value is ServiceReadScope[];
185
+ /** printableReference accepts a grant's external_ref: 1 to 128 printable ASCII characters. */
186
+ export declare function printableReference(value: unknown): value is string;
173
187
  export {};
package/dist/slice4.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ClientFlowError, canonicalDecimal, deadlineAfter, identityMethods, integer, isNextAction, opaqueId, record, rfc3339UTC, unexpiredRFC3339, } from "./auth.js";
2
2
  import { parseWalletLimitsDisplay } from "./walletLimits.js";
3
+ import { parseDebitPermissionDisplay, parseDebitPermissionRevokeDisplay } from "./debitPermissionWire.js";
4
+ import { parseConsentDebitOutcome, parseDebitTerms } from "./consentDebit.js";
3
5
  import { parseLinkedEnrollmentDisplay, parseLinkedRequestGrantResult, validateLinkedEnrollmentCreate, } from "./modeCLinked.js";
4
6
  /** parseIdentityList accepts the closed answer of GET /v1/identities. */
5
7
  export function parseIdentityList(payload) {
@@ -202,9 +204,9 @@ export function parseOperationConfirmationCallback(value, expectedRedirect) {
202
204
  }
203
205
  export function parseLocalOperationResult(payload) {
204
206
  const root = record(payload, ["request_id", "result"]);
205
- const result = record(root.result, ["kind", "target_id", "new_version", "state"], ["grant"]);
207
+ const result = record(root.result, ["kind", "target_id", "new_version", "state"], ["grant", "debit"]);
206
208
  if (!opaqueId(root.request_id) || !localResultKind(result.kind) || !opaqueId(result.target_id) || !canonicalDecimal(result.new_version) || (result.state !== "active" && result.state !== "revoked") ||
207
- (result.grant !== undefined) !== (result.kind === "request_grant")) {
209
+ (result.grant !== undefined) !== (result.kind === "request_grant") || (result.debit !== undefined && result.kind !== "service_grant")) {
208
210
  throw new ClientFlowError("invalid_server_response");
209
211
  }
210
212
  const parsed = { kind: result.kind, targetId: result.target_id, newVersion: result.new_version, state: result.state };
@@ -213,6 +215,8 @@ export function parseLocalOperationResult(payload) {
213
215
  if (parsed.grant.requestGrantId !== result.target_id)
214
216
  throw new ClientFlowError("invalid_server_response");
215
217
  }
218
+ if (result.debit !== undefined)
219
+ parsed.debit = parseConsentDebitOutcome(result.debit);
216
220
  return { requestId: root.request_id, result: parsed };
217
221
  }
218
222
  function parseDisplay(value, kind) {
@@ -232,28 +236,36 @@ function parseDisplay(value, kind) {
232
236
  return parseWalletLimitsDisplay(value);
233
237
  if (kind === "session.authorize_requests")
234
238
  return parseLinkedEnrollmentDisplay(value);
239
+ if (kind === "debit_permission.create")
240
+ return parseDebitPermissionDisplay(value);
241
+ if (kind === "debit_permission.revoke")
242
+ return parseDebitPermissionRevokeDisplay(value);
235
243
  if (kind === "session.revoke") {
236
244
  const display = record(value, ["kind", "owner_id", "requester_id", "session_id"]);
237
245
  if (display.kind !== kind || !opaqueId(display.owner_id) || !opaqueId(display.requester_id) || !opaqueId(display.session_id))
238
246
  throw new ClientFlowError("invalid_server_response");
239
247
  return { kind, ownerId: display.owner_id, requesterId: display.requester_id, sessionId: display.session_id };
240
248
  }
241
- const display = record(value, ["kind", "owner_id", "requester_id", "wallet_id", "service_principal_id", "enabled", "scopes"], ["external_ref"]);
249
+ const display = record(value, ["kind", "owner_id", "requester_id", "wallet_id", "service_principal_id", "enabled", "scopes"], ["external_ref", "debit"]);
242
250
  if (display.kind !== kind || !opaqueId(display.owner_id) || !opaqueId(display.requester_id) || !opaqueId(display.wallet_id) || !opaqueId(display.service_principal_id) || typeof display.enabled !== "boolean" || !serviceScopes(display.scopes) || (display.external_ref !== undefined && !printableReference(display.external_ref)))
243
251
  throw new ClientFlowError("invalid_server_response");
244
252
  const result = { kind, ownerId: display.owner_id, requesterId: display.requester_id, walletId: display.wallet_id, servicePrincipalId: display.service_principal_id, enabled: display.enabled, scopes: display.scopes };
245
253
  if (display.external_ref !== undefined)
246
254
  result.externalRef = display.external_ref;
255
+ if (display.debit !== undefined)
256
+ result.debit = parseDebitTerms(display.debit);
247
257
  return result;
248
258
  }
249
259
  function localOperationKind(value) {
250
- return value === "identity.link" || value === "identity.unlink" || value === "session.revoke" || value === "service_grant.set" || value === "wallet_limits.set" || value === "session.authorize_requests";
260
+ return value === "identity.link" || value === "identity.unlink" || value === "session.revoke" || value === "service_grant.set" || value === "wallet_limits.set" || value === "session.authorize_requests" ||
261
+ value === "debit_permission.create" || value === "debit_permission.revoke";
251
262
  }
252
263
  function localResultKind(value) {
253
- return value === "identity" || value === "session" || value === "service_grant" || value === "wallet_limits" || value === "request_grant";
264
+ return value === "identity" || value === "session" || value === "service_grant" || value === "wallet_limits" || value === "request_grant" || value === "debit_permission";
254
265
  }
255
266
  function operationState(value) {
256
- return value === "awaiting_confirmation" || value === "authorized" || value === "succeeded" || value === "denied" || value === "canceled" || value === "expired";
267
+ return value === "awaiting_confirmation" || value === "authorized" || value === "pending_target_fence" || value === "succeeded" || value === "failed" ||
268
+ value === "denied" || value === "canceled" || value === "expired";
257
269
  }
258
270
  function effectState(value) {
259
271
  return value === "no_effect" || value === "known";
@@ -264,12 +276,14 @@ function lowerHexDigest(value) {
264
276
  function base64url(value) {
265
277
  return typeof value === "string" && /^[A-Za-z0-9_-]+$/.test(value);
266
278
  }
267
- function serviceScopes(value) {
279
+ /** serviceScopes accepts the read scopes of a service grant: at most the three, each once (none: a withdrawal). */
280
+ export function serviceScopes(value) {
268
281
  if (!Array.isArray(value) || value.length > 3 || new Set(value).size !== value.length)
269
282
  return false;
270
283
  return value.every((scope) => scope === "wallet:read" || scope === "operation:read" || scope === "streams:read");
271
284
  }
272
- function printableReference(value) {
285
+ /** printableReference accepts a grant's external_ref: 1 to 128 printable ASCII characters. */
286
+ export function printableReference(value) {
273
287
  return typeof value === "string" && /^[\x20-\x7E]{1,128}$/.test(value);
274
288
  }
275
289
  function httpsURL(value) {
package/dist/streams.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Authorized Streams reads (source §1.7, §1.11, §2.2, §2.6).
2
2
  //
3
3
  // This module is the package's third entry point
4
- // (`@shuttle.technology/authorization/streams`). It owns exactly the client half of the
4
+ // (`@streams/authorization/streams`). It owns exactly the client half of the
5
5
  // served reads: it builds their exact `/v1` paths, enforces the page bounds
6
6
  // of source §2.8 before a request leaves the client, and parses the two
7
7
  // closed response shapes. Events are read in one shape, wallet events 3.0,
@@ -0,0 +1,62 @@
1
+ import { type AuthorizationErrorCode } from "./errors.ts";
2
+ import { type ServiceReadScope } from "./slice4.ts";
3
+ import type { UserWalletOperation } from "./walletOperations.ts";
4
+ /** The grant a creation asks for: the service, one to three read scopes, and the service's optional reference for its user. */
5
+ export interface WalletCreateGrant {
6
+ servicePrincipalId: string;
7
+ scopes: ServiceReadScope[];
8
+ externalRef?: string;
9
+ }
10
+ /** What createWallet() takes: the network, the app's own device key if it keeps the share itself, and the optional grant. */
11
+ export interface WalletCreateInput {
12
+ network: string;
13
+ recipientPublicKey?: string;
14
+ /**
15
+ * Opens the new wallet to a service in the same passkey confirmation. When that service holds wallet:debit:user,
16
+ * the same confirmation also creates its debit permission on the wallet with the ceilings the platform set for the
17
+ * service (display.debit names them, the provider's page says them); there is nothing to pass for it, and
18
+ * walletOperationResult() reports it in result.debit. That needs the share the SDK keeps (no recipientPublicKey).
19
+ */
20
+ grant?: WalletCreateGrant;
21
+ }
22
+ /**
23
+ * What became of the grant once the wallet exists. succeeded: the service
24
+ * reads the wallet. pending_target_fence: a first streams:read grant waits
25
+ * for Streams; read client.operation(operationId) until it ends. not_set: the
26
+ * grant was not set, for the reason code says; DEPENDENCY_UNAVAILABLE passes,
27
+ * and reading the result again repeats the step, any other code asks for the
28
+ * grant alone. linkedWalletId is the created wallet among the linked wallets.
29
+ */
30
+ export interface WalletCreateGrantOutcome {
31
+ state: "succeeded" | "pending_target_fence" | "not_set";
32
+ operationId?: string;
33
+ linkedWalletId?: string;
34
+ code?: AuthorizationErrorCode;
35
+ }
36
+ /** The grant's wire member, the scopes in the provider's order. */
37
+ export type WalletCreateGrantWire = {
38
+ service_principal_id: string;
39
+ scopes: ServiceReadScope[];
40
+ external_ref?: string;
41
+ };
42
+ /** validWalletCreateGrant reads the wire member as the provider takes it: one of its services, one to three scopes, an optional printable reference. */
43
+ export declare function validWalletCreateGrant(value: unknown): value is WalletCreateGrantWire;
44
+ /** walletCreateBody is the closed wallet.create body of createWallet(): the device key the creation seals the share to, and the grant when one is asked for. */
45
+ export declare function walletCreateBody(ownerId: string, input: WalletCreateInput, sdkKey: string | undefined): {
46
+ kind: "wallet.create";
47
+ owner_type: "account";
48
+ owner_id: string;
49
+ payload: {
50
+ network: string;
51
+ recipient_public_key: string;
52
+ };
53
+ grant?: WalletCreateGrantWire;
54
+ };
55
+ /** createdAsAsked refuses an admitted creation whose display names another grant than the one asked for, or one where none was. */
56
+ export declare function createdAsAsked(operation: UserWalletOperation, create: {
57
+ grant?: WalletCreateGrantWire;
58
+ }): UserWalletOperation;
59
+ /** parseWalletCreateGrant reads the grant a creation's display names. */
60
+ export declare function parseWalletCreateGrant(value: unknown): WalletCreateGrant;
61
+ /** parseWalletCreateGrantOutcome reads the grant member of a creation's result: a code exactly for not_set, an operation for every grant that was set. */
62
+ export declare function parseWalletCreateGrantOutcome(value: unknown): WalletCreateGrantOutcome;
@@ -0,0 +1,89 @@
1
+ // A wallet's creation that opens the wallet to a service in the same passkey
2
+ // confirmation (owner decision of 28 October 2026): the read grant
3
+ // service_grant.set would give, named beside the wallet. The person confirms
4
+ // once, on a page that names the service and what it may see; the provider
5
+ // sets the grant when the creation's result is read. A grant that cannot be
6
+ // set leaves the wallet as it is, and the result says why: the app then asks
7
+ // for the grant alone (createServiceGrant), with its own passkey confirmation.
8
+ import { ClientFlowError, opaqueId, record } from "./auth.js";
9
+ import { isAuthorizationErrorCode } from "./errors.js";
10
+ import { printableReference, serviceScopes } from "./slice4.js";
11
+ import { lowerUUID } from "./walletDisplayParts.js";
12
+ const order = ["wallet:read", "operation:read", "streams:read"];
13
+ /** validWalletCreateGrant reads the wire member as the provider takes it: one of its services, one to three scopes, an optional printable reference. */
14
+ export function validWalletCreateGrant(value) {
15
+ if (value === null || typeof value !== "object" || Array.isArray(value))
16
+ return false;
17
+ const grant = value;
18
+ const members = Object.keys(grant);
19
+ return members.every((member) => member === "service_principal_id" || member === "scopes" || member === "external_ref") &&
20
+ opaqueId(grant.service_principal_id) && serviceScopes(grant.scopes) && grant.scopes.length >= 1 && (grant.external_ref === undefined || printableReference(grant.external_ref));
21
+ }
22
+ function grantWire(input) {
23
+ let declared;
24
+ try {
25
+ declared = record(input, ["servicePrincipalId", "scopes"], ["externalRef"]);
26
+ }
27
+ catch {
28
+ throw new ClientFlowError("invalid_operation_create");
29
+ }
30
+ // The scopes go in the provider's order, so a repeat of the request is the same request.
31
+ const raw = declared.scopes;
32
+ const scopes = serviceScopes(raw) ? order.filter((scope) => raw.includes(scope)) : raw;
33
+ const wire = { service_principal_id: declared.servicePrincipalId, scopes: scopes };
34
+ if (declared.externalRef !== undefined)
35
+ wire.external_ref = declared.externalRef;
36
+ if (!validWalletCreateGrant(wire))
37
+ throw new ClientFlowError("invalid_operation_create");
38
+ return wire;
39
+ }
40
+ /** walletCreateBody is the closed wallet.create body of createWallet(): the device key the creation seals the share to, and the grant when one is asked for. */
41
+ export function walletCreateBody(ownerId, input, sdkKey) {
42
+ if (input === null || typeof input !== "object")
43
+ throw new ClientFlowError("invalid_operation_create");
44
+ const create = { kind: "wallet.create", owner_type: "account", owner_id: ownerId, payload: { network: input.network, recipient_public_key: sdkKey ?? input.recipientPublicKey } };
45
+ if (input.grant !== undefined)
46
+ create.grant = grantWire(input.grant);
47
+ return create;
48
+ }
49
+ function sameGrant(shown, asked) {
50
+ if (shown === undefined || asked === undefined)
51
+ return shown === asked;
52
+ return shown.servicePrincipalId === asked.service_principal_id && shown.scopes.join(" ") === asked.scopes.join(" ") && shown.externalRef === asked.external_ref;
53
+ }
54
+ /** createdAsAsked refuses an admitted creation whose display names another grant than the one asked for, or one where none was. */
55
+ export function createdAsAsked(operation, create) {
56
+ const display = operation.display;
57
+ if (operation.kind !== "wallet.create" || display.kind !== "wallet.create")
58
+ throw new ClientFlowError("invalid_server_response");
59
+ if (!("materialPurged" in display) && !sameGrant(display.grant, create.grant))
60
+ throw new ClientFlowError("invalid_server_response");
61
+ return operation;
62
+ }
63
+ /** parseWalletCreateGrant reads the grant a creation's display names. */
64
+ export function parseWalletCreateGrant(value) {
65
+ if (!validWalletCreateGrant(value) || order.filter((scope) => value.scopes.includes(scope)).join(" ") !== value.scopes.join(" "))
66
+ throw new ClientFlowError("invalid_server_response");
67
+ const grant = { servicePrincipalId: value.service_principal_id, scopes: [...value.scopes] };
68
+ if (value.external_ref !== undefined)
69
+ grant.externalRef = value.external_ref;
70
+ return grant;
71
+ }
72
+ /** parseWalletCreateGrantOutcome reads the grant member of a creation's result: a code exactly for not_set, an operation for every grant that was set. */
73
+ export function parseWalletCreateGrantOutcome(value) {
74
+ const outcome = record(value, ["state"], ["operation_id", "linked_wallet_id", "code"]);
75
+ const state = outcome.state;
76
+ if ((state !== "succeeded" && state !== "pending_target_fence" && state !== "not_set") || (outcome.operation_id !== undefined && !lowerUUID(outcome.operation_id)) ||
77
+ (outcome.linked_wallet_id !== undefined && !lowerUUID(outcome.linked_wallet_id)) || (state === "not_set") !== (outcome.code !== undefined) ||
78
+ (outcome.code !== undefined && !isAuthorizationErrorCode(outcome.code)) || (state !== "not_set" && outcome.operation_id === undefined)) {
79
+ throw new ClientFlowError("invalid_server_response");
80
+ }
81
+ const parsed = { state };
82
+ if (outcome.operation_id !== undefined)
83
+ parsed.operationId = outcome.operation_id;
84
+ if (outcome.linked_wallet_id !== undefined)
85
+ parsed.linkedWalletId = outcome.linked_wallet_id;
86
+ if (outcome.code !== undefined)
87
+ parsed.code = outcome.code;
88
+ return parsed;
89
+ }
@@ -13,3 +13,5 @@ export declare function summaryOf(value: unknown): {
13
13
  export declare function rawQuantity(value: unknown, positive: boolean): value is string;
14
14
  export declare function bindingRevision(value: unknown): value is string;
15
15
  export declare function lowerUUID(value: unknown): value is string;
16
+ /** lowerHexBytes accepts 1..1024 bytes of lowercase hexadecimal. */
17
+ export declare function lowerHexBytes(value: unknown): value is string;
@@ -41,3 +41,7 @@ export function bindingRevision(value) {
41
41
  export function lowerUUID(value) {
42
42
  return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value);
43
43
  }
44
+ /** lowerHexBytes accepts 1..1024 bytes of lowercase hexadecimal. */
45
+ export function lowerHexBytes(value) {
46
+ return typeof value === "string" && value.length >= 2 && value.length <= 2048 && value.length % 2 === 0 && /^[0-9a-f]+$/.test(value);
47
+ }