@shuttle.technology/authorization 0.0.0-stage → 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 (93) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +1124 -2
  3. package/dist/accountLists.d.ts +28 -0
  4. package/dist/accountLists.js +27 -0
  5. package/dist/aesGcmSiv.d.ts +14 -0
  6. package/dist/aesGcmSiv.js +160 -0
  7. package/dist/auth.d.ts +68 -0
  8. package/dist/auth.js +189 -0
  9. package/dist/balances.d.ts +31 -0
  10. package/dist/balances.js +44 -0
  11. package/dist/canonicalize.d.ts +19 -0
  12. package/dist/canonicalize.js +107 -0
  13. package/dist/client.d.ts +152 -0
  14. package/dist/client.js +781 -0
  15. package/dist/clientTypes.d.ts +56 -0
  16. package/dist/clientTypes.js +5 -0
  17. package/dist/configuration.d.ts +48 -0
  18. package/dist/configuration.js +72 -0
  19. package/dist/consentDebit.d.ts +36 -0
  20. package/dist/consentDebit.js +95 -0
  21. package/dist/credentials.d.ts +116 -0
  22. package/dist/credentials.js +315 -0
  23. package/dist/debitPermissionWire.d.ts +90 -0
  24. package/dist/debitPermissionWire.js +118 -0
  25. package/dist/debitPermissions.d.ts +39 -0
  26. package/dist/debitPermissions.js +117 -0
  27. package/dist/deviceKey.d.ts +13 -0
  28. package/dist/deviceKey.js +28 -0
  29. package/dist/deviceRecovery.d.ts +40 -0
  30. package/dist/deviceRecovery.js +143 -0
  31. package/dist/deviceShare.d.ts +9 -0
  32. package/dist/deviceShare.js +24 -0
  33. package/dist/deviceShareEnvelope.d.ts +13 -0
  34. package/dist/deviceShareEnvelope.js +139 -0
  35. package/dist/deviceShares.d.ts +91 -0
  36. package/dist/deviceShares.js +204 -0
  37. package/dist/errors.d.ts +280 -0
  38. package/dist/errors.js +85 -0
  39. package/dist/linkedChecks.d.ts +17 -0
  40. package/dist/linkedChecks.js +31 -0
  41. package/dist/linkedRecovery.d.ts +40 -0
  42. package/dist/linkedRecovery.js +49 -0
  43. package/dist/linkedRegistrations.d.ts +11 -0
  44. package/dist/linkedRegistrations.js +109 -0
  45. package/dist/linkedSendDisplay.d.ts +73 -0
  46. package/dist/linkedSendDisplay.js +84 -0
  47. package/dist/linkedWalletSignals.d.ts +22 -0
  48. package/dist/linkedWalletSignals.js +101 -0
  49. package/dist/linkedWallets.d.ts +401 -0
  50. package/dist/linkedWallets.js +436 -0
  51. package/dist/me.d.ts +27 -0
  52. package/dist/me.js +30 -0
  53. package/dist/mediatedOperation.d.ts +27 -0
  54. package/dist/mediatedOperation.js +68 -0
  55. package/dist/mobile.d.ts +64 -0
  56. package/dist/mobile.js +69 -0
  57. package/dist/modeC.d.ts +131 -0
  58. package/dist/modeC.js +248 -0
  59. package/dist/modeCLinked.d.ts +207 -0
  60. package/dist/modeCLinked.js +270 -0
  61. package/dist/pages.d.ts +22 -0
  62. package/dist/pages.js +45 -0
  63. package/dist/poll.d.ts +15 -0
  64. package/dist/poll.js +76 -0
  65. package/dist/redirects.d.ts +45 -0
  66. package/dist/redirects.js +108 -0
  67. package/dist/requestGrant.d.ts +169 -0
  68. package/dist/requestGrant.js +243 -0
  69. package/dist/sends.d.ts +89 -0
  70. package/dist/sends.js +127 -0
  71. package/dist/serviceGrants.d.ts +52 -0
  72. package/dist/serviceGrants.js +46 -0
  73. package/dist/slice4.d.ts +187 -0
  74. package/dist/slice4.js +299 -0
  75. package/dist/streams.d.ts +212 -0
  76. package/dist/streams.js +282 -0
  77. package/dist/trackedWallets.d.ts +53 -0
  78. package/dist/trackedWallets.js +104 -0
  79. package/dist/transferStatus.d.ts +45 -0
  80. package/dist/transferStatus.js +43 -0
  81. package/dist/walletCreateGrant.d.ts +62 -0
  82. package/dist/walletCreateGrant.js +89 -0
  83. package/dist/walletDisplayParts.d.ts +17 -0
  84. package/dist/walletDisplayParts.js +47 -0
  85. package/dist/walletEvents.d.ts +43 -0
  86. package/dist/walletEvents.js +97 -0
  87. package/dist/walletLimits.d.ts +65 -0
  88. package/dist/walletLimits.js +102 -0
  89. package/dist/walletOperations.d.ts +209 -0
  90. package/dist/walletOperations.js +317 -0
  91. package/dist/webRenewal.d.ts +92 -0
  92. package/dist/webRenewal.js +179 -0
  93. package/package.json +65 -3
@@ -0,0 +1,179 @@
1
+ // The web session's renewal (owner decision of 27 October 2026). The refresh
2
+ // credential is the provider's HttpOnly cookie, partitioned to the site the
3
+ // application runs on, so a background call from the application carries it
4
+ // and no navigation is needed, also after a reload or in a new tab. When the
5
+ // browser sends no cookie the call is refused and the SDK raises
6
+ // web_renewal_requires_navigation: the application then starts the top-level
7
+ // restore (restoreSession) itself. The SDK never navigates on its own. The
8
+ // restore sequence is here too, under the same lock.
9
+ import { abortable, ClientFlowError, newHandoffBinding, opaqueId, parseAuthorizationError, record } from "./auth.js";
10
+ import { restoreCallback } from "./redirects.js";
11
+ /** The code of the typed error that asks the application to start the top-level restore. */
12
+ export const webRenewalRequiresNavigation = "web_renewal_requires_navigation";
13
+ /**
14
+ * The tabs of one application share the cookie, and the provider ends a
15
+ * session whose refresh value it is shown twice. Every renewal and every
16
+ * restore therefore runs under one Web Lock per client: each sends the value
17
+ * the previous one left. A restore holds it until the host's launcher
18
+ * settles or the restore's signal aborts it; the host gets the same signal
19
+ * and closes its window, so no abandoned navigation rotates the value later.
20
+ * A sign-in's exchange, which sets the cookie too, goes out under the same
21
+ * lock (client.ts), so a renewal's late answer never follows a newer
22
+ * sign-in's.
23
+ */
24
+ export function underRenewalLock(target, work) {
25
+ const locks = globalThis.navigator?.locks;
26
+ if (!locks || typeof locks.request !== "function")
27
+ throw new ClientFlowError("runtime_capability_unavailable");
28
+ const whenWaiting = target.whenWaiting;
29
+ if (!whenWaiting)
30
+ return locks.request(target.lockName, { signal: target.signal }, work);
31
+ // Told when the lock is not free at once, the caller learns it waits; the work still runs once it is.
32
+ return locks.request(target.lockName, { ifAvailable: true }, async (lock) => (lock ? { ran: await work() } : undefined)).then((done) => {
33
+ if (done)
34
+ return done.ran;
35
+ whenWaiting();
36
+ return locks.request(target.lockName, { signal: target.signal }, work);
37
+ });
38
+ }
39
+ /**
40
+ * renewWebSession is the background call: POST /v1/auth/renew with the cookie
41
+ * (credentials: "include"). A page that holds the session's CSRF token sends
42
+ * it; a page that holds none (a reload, a new tab), or whose token a renewal
43
+ * or restore in another tab replaced, renews from the cookie alone with the
44
+ * fixed X-Authorization-Renew header and gets a new token. It answers the
45
+ * session payload.
46
+ */
47
+ export async function renewWebSession(request) {
48
+ return underRenewalLock(request, async () => {
49
+ if (request.csrfToken) {
50
+ request.current?.();
51
+ const renewed = await renewOnce(request, { "X-CSRF-Token": request.csrfToken });
52
+ if (renewed.ok)
53
+ return renewed.payload;
54
+ if (renewed.refusal.status !== 403 || renewed.refusal.code !== "ORIGIN_NOT_ALLOWED")
55
+ throw refusedRenewal(renewed.refusal);
56
+ }
57
+ request.current?.();
58
+ const renewed = await renewOnce(request, { "X-Authorization-Renew": "1" });
59
+ if (renewed.ok)
60
+ return renewed.payload;
61
+ throw refusedRenewal(renewed.refusal);
62
+ });
63
+ }
64
+ /**
65
+ * staleCsrfRefusal is the answer a web lifecycle call gets when a renewal or
66
+ * restore in another tab replaced the session's CSRF token since this page
67
+ * last renewed.
68
+ */
69
+ export function staleCsrfRefusal(error) {
70
+ return error instanceof Error && error.status === 403 && error.code === "ORIGIN_NOT_ALLOWED";
71
+ }
72
+ /**
73
+ * revokeWithCurrentCredentials is logout's way past a CSRF token another tab
74
+ * replaced. Under the tabs' lock, held until the revocation answers, it
75
+ * renews from the cookie alone and revokes with the tokens that answer, but
76
+ * only when they belong to the session the logout was asked for: a cookie
77
+ * another sign-in replaced is left alone and the revocation is not confirmed.
78
+ */
79
+ export async function revokeWithCurrentCredentials(request, revoke) {
80
+ return underRenewalLock(request, async () => {
81
+ const renewed = await renewOnce({ ...request, csrfToken: undefined }, { "X-Authorization-Renew": "1" });
82
+ if (!renewed.ok)
83
+ throw refusedRenewal(renewed.refusal);
84
+ const payload = renewed.payload;
85
+ if (!payload || typeof payload.access_token !== "string" || payload.access_token === "" || typeof payload.csrf_token !== "string" || payload.csrf_token === "") {
86
+ throw new ClientFlowError("invalid_server_response");
87
+ }
88
+ if (payload.session_id !== request.sessionId)
89
+ throw new ClientFlowError("session_unavailable");
90
+ return revoke(payload.access_token, payload.csrf_token);
91
+ });
92
+ }
93
+ /** revokedAnswer says a logout answer confirms the revocation. */
94
+ export function revokedAnswer(payload) {
95
+ try {
96
+ const result = record(payload, ["request_id", "server_disposition"]);
97
+ return opaqueId(result.request_id) && result.server_disposition === "server_revoked";
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * revokeUnclaimedSession ends the session of a sign-in answer this page does
105
+ * not install, because a logout or a newer sign-in overtook it while it was
106
+ * asked for. The answer set the cookie, so without it a reload would take up
107
+ * a session nobody holds, one the person may have logged out of. The
108
+ * provider clears the cookie only while it holds that session. A CSRF token
109
+ * another tab replaced meanwhile (it renewed that session from the cookie) is
110
+ * followed by the logout's recovery: one renewal from the cookie under the
111
+ * tabs' lock, checked against the answer's session, and the revocation with
112
+ * its tokens. It answers true only for a confirmed revocation of exactly that
113
+ * session; a cookie that holds another session is left alone and answers
114
+ * false, as does any refusal or failure. whenWaiting is told when the
115
+ * recovery has to wait for the lock (a restore may hold it while its window
116
+ * is open): the revocation goes on, but nobody needs to wait for its verdict.
117
+ */
118
+ export async function revokeUnclaimedSession(request, payload, revoke) {
119
+ const answer = payload;
120
+ if (!answer || typeof answer !== "object" || typeof answer.access_token !== "string" || answer.access_token === "" ||
121
+ typeof answer.csrf_token !== "string" || answer.csrf_token === "" || !opaqueId(answer.session_id))
122
+ return false;
123
+ const sessionId = answer.session_id;
124
+ try {
125
+ return revokedAnswer(await revoke(answer.access_token, answer.csrf_token));
126
+ }
127
+ catch (error) {
128
+ if (!staleCsrfRefusal(error))
129
+ return false;
130
+ }
131
+ try {
132
+ return revokedAnswer(await revokeWithCurrentCredentials({ ...request, sessionId }, revoke));
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ async function renewOnce(request, header) {
139
+ const response = await request.fetch(`${request.authorizationOrigin}/v1/auth/renew`, {
140
+ method: "POST",
141
+ headers: { Accept: "application/json", "Content-Type": "application/json", ...header },
142
+ body: JSON.stringify({ client_id: request.clientId }),
143
+ credentials: "include",
144
+ cache: "no-store",
145
+ signal: request.signal,
146
+ });
147
+ const payload = await response.json();
148
+ return response.ok ? { ok: true, payload } : { ok: false, refusal: parseAuthorizationError(payload, response.status) };
149
+ }
150
+ // 401: the browser sent no cookie (it keeps no partitioned cookie) or the
151
+ // value is no longer live; the top-level restore is the way back. Any other
152
+ // refusal is passed on as it came.
153
+ function refusedRenewal(refusal) {
154
+ return refusal.status === 401 && refusal.code === "UNAUTHORIZED" ? new ClientFlowError(webRenewalRequiresNavigation) : refusal;
155
+ }
156
+ /**
157
+ * restoreWebSession is the top-level restore: a fresh state and S256 verifier
158
+ * in memory, the provider restore URL through the host's launcher, and the
159
+ * one-use handoff of the redirect it returns exchanged for the session.
160
+ */
161
+ export async function restoreWebSession(request) {
162
+ return underRenewalLock(request, async () => {
163
+ const binding = await newHandoffBinding(request.crypto);
164
+ request.signal?.throwIfAborted();
165
+ const start = new URL("/v1/auth/restore", request.authorizationOrigin);
166
+ for (const [name, value] of [["client_id", request.clientId], ["redirect_uri", request.redirectUri], ["state", binding.state], ["code_challenge", binding.challenge], ["code_challenge_method", "S256"]]) {
167
+ start.searchParams.set(name, value);
168
+ }
169
+ request.current?.();
170
+ const returned = await abortable(request.launch(start.toString(), request.signal), request.signal);
171
+ if (typeof returned !== "string")
172
+ throw new ClientFlowError("restore_redirect_unavailable");
173
+ const handoff = restoreCallback(returned, request.redirectUri);
174
+ if (handoff.state !== binding.state)
175
+ throw new ClientFlowError("state_mismatch");
176
+ request.current?.();
177
+ return request.exchange(JSON.stringify({ client_id: request.clientId, restore_id: handoff.restoreId, code: handoff.code, verifier: binding.verifier, redirect_uri: request.redirectUri }));
178
+ });
179
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@shuttle.technology/authorization",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Browser and mobile client of the Streams Authorization API: hosted sign-in, the account's wallets, service grants, limits, Mode B sends and Mode C signed requests.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist/**/*.js",
9
+ "dist/**/*.d.ts",
10
+ "README.md",
11
+ "CHANGELOG.md"
12
+ ],
13
+ "main": "./dist/client.js",
14
+ "types": "./dist/client.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/client.d.ts",
18
+ "default": "./dist/client.js"
19
+ },
20
+ "./mode-c": {
21
+ "types": "./dist/modeC.d.ts",
22
+ "default": "./dist/modeC.js"
23
+ },
24
+ "./mode-c-linked": {
25
+ "types": "./dist/modeCLinked.d.ts",
26
+ "default": "./dist/modeCLinked.js"
27
+ },
28
+ "./streams": {
29
+ "types": "./dist/streams.d.ts",
30
+ "default": "./dist/streams.js"
31
+ },
32
+ "./linked-wallets": {
33
+ "types": "./dist/linkedWallets.d.ts",
34
+ "default": "./dist/linkedWallets.js"
35
+ },
36
+ "./debit-permissions": {
37
+ "types": "./dist/debitPermissions.d.ts",
38
+ "default": "./dist/debitPermissions.js"
39
+ },
40
+ "./device-key": {
41
+ "types": "./dist/deviceKey.d.ts",
42
+ "default": "./dist/deviceKey.js"
43
+ },
44
+ "./device-share": {
45
+ "types": "./dist/deviceShare.d.ts",
46
+ "default": "./dist/deviceShare.js"
47
+ }
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "24.13.3",
58
+ "typescript": "7.0.2"
59
+ },
60
+ "scripts": {
61
+ "check": "node --experimental-strip-types --check src/accountLists.ts && node --experimental-strip-types --check src/auth.ts && node --experimental-strip-types --check src/balances.ts && node --experimental-strip-types --check src/canonicalize.ts && node --experimental-strip-types --check src/client.ts && node --experimental-strip-types --check src/clientTypes.ts && node --experimental-strip-types --check src/deviceRecovery.ts && node --experimental-strip-types --check src/deviceShares.ts && node --experimental-strip-types --check src/linkedRecovery.ts && node --experimental-strip-types --check src/linkedRegistrations.ts && node --experimental-strip-types --check src/configuration.ts && node --experimental-strip-types --check src/consentDebit.ts && node --experimental-strip-types --check src/credentials.ts && node --experimental-strip-types --check src/debitPermissions.ts && node --experimental-strip-types --check src/debitPermissionWire.ts && node --experimental-strip-types --check src/aesGcmSiv.ts && node --experimental-strip-types --check src/deviceKey.ts && node --experimental-strip-types --check src/deviceShare.ts && node --experimental-strip-types --check src/deviceShareEnvelope.ts && node --experimental-strip-types --check src/errors.ts && node --experimental-strip-types --check src/linkedSendDisplay.ts && node --experimental-strip-types --check src/linkedChecks.ts && node --experimental-strip-types --check src/linkedWallets.ts && node --experimental-strip-types --check src/linkedWalletSignals.ts && node --experimental-strip-types --check src/me.ts && node --experimental-strip-types --check src/mediatedOperation.ts && node --experimental-strip-types --check src/mobile.ts && node --experimental-strip-types --check src/pages.ts && node --experimental-strip-types --check src/modeC.ts && node --experimental-strip-types --check src/modeCLinked.ts && node --experimental-strip-types --check src/poll.ts && node --experimental-strip-types --check src/redirects.ts && node --experimental-strip-types --check src/requestGrant.ts && node --experimental-strip-types --check src/sends.ts && node --experimental-strip-types --check src/serviceGrants.ts && node --experimental-strip-types --check src/slice4.ts && node --experimental-strip-types --check src/streams.ts && node --experimental-strip-types --check src/trackedWallets.ts && node --experimental-strip-types --check src/transferStatus.ts && node --experimental-strip-types --check src/walletCreateGrant.ts && node --experimental-strip-types --check src/walletDisplayParts.ts && node --experimental-strip-types --check src/walletEvents.ts && node --experimental-strip-types --check src/walletLimits.ts && node --experimental-strip-types --check src/walletOperations.ts && node --experimental-strip-types --check src/webRenewal.ts && node --check test/http-integration.mjs && node --check test/http-integration-slice4.mjs && node --check test/http-integration-slice6.mjs && node --check test/http-integration-slice7.mjs && node --check test/http-integration-contract.mjs && node --check test/http-integration-linked-send.mjs && node --check test/http-integration-limits.mjs && node --check test/http-integration-modec-linked.mjs && node --check test/http-integration-mode-a.mjs && node --check test/http-integration-mode-a-platform.mjs && node --check test/http-integration-demo.mjs && node --check test/http-integration-wallet-create.mjs && node --check test/http-integration-wallet-create-grant.mjs && node --check test/http-integration-wallet-create-debit.mjs",
62
+ "typecheck": "tsc --noEmit",
63
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
64
+ "test": "node --experimental-strip-types --test test/*.test.ts",
65
+ "integration:http": "node test/http-integration.mjs",
66
+ "verify": "pnpm run check && pnpm run typecheck && pnpm run build && pnpm run test"
67
+ }
6
68
  }