@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.
- package/CHANGELOG.md +191 -0
- package/README.md +1082 -666
- package/dist/accountLists.d.ts +28 -0
- package/dist/accountLists.js +27 -0
- package/dist/auth.d.ts +11 -11
- package/dist/auth.js +28 -54
- package/dist/client.d.ts +62 -174
- package/dist/client.js +381 -354
- package/dist/clientTypes.d.ts +56 -0
- package/dist/clientTypes.js +5 -0
- package/dist/configuration.d.ts +48 -0
- package/dist/configuration.js +72 -0
- package/dist/consentDebit.d.ts +36 -0
- package/dist/consentDebit.js +95 -0
- package/dist/credentials.d.ts +116 -0
- package/dist/credentials.js +315 -0
- package/dist/debitPermissionWire.d.ts +90 -0
- package/dist/debitPermissionWire.js +118 -0
- package/dist/debitPermissions.d.ts +39 -0
- package/dist/debitPermissions.js +117 -0
- package/dist/deviceRecovery.d.ts +40 -0
- package/dist/deviceRecovery.js +143 -0
- package/dist/deviceShares.d.ts +91 -0
- package/dist/deviceShares.js +204 -0
- package/dist/errors.d.ts +280 -0
- package/dist/errors.js +85 -0
- package/dist/linkedRecovery.d.ts +40 -0
- package/dist/linkedRecovery.js +49 -0
- package/dist/linkedRegistrations.d.ts +11 -0
- package/dist/linkedRegistrations.js +109 -0
- package/dist/linkedWallets.d.ts +46 -18
- package/dist/linkedWallets.js +85 -120
- package/dist/me.d.ts +27 -0
- package/dist/me.js +30 -0
- package/dist/mobile.d.ts +64 -0
- package/dist/mobile.js +69 -0
- package/dist/redirects.d.ts +45 -0
- package/dist/redirects.js +108 -0
- package/dist/serviceGrants.d.ts +52 -0
- package/dist/serviceGrants.js +46 -0
- package/dist/slice4.d.ts +20 -6
- package/dist/slice4.js +22 -8
- package/dist/streams.js +1 -1
- package/dist/walletCreateGrant.d.ts +62 -0
- package/dist/walletCreateGrant.js +89 -0
- package/dist/walletDisplayParts.d.ts +2 -0
- package/dist/walletDisplayParts.js +4 -0
- package/dist/walletOperations.d.ts +23 -6
- package/dist/walletOperations.js +31 -22
- package/dist/webRenewal.d.ts +92 -0
- package/dist/webRenewal.js +179 -0
- package/package.json +25 -23
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { type NextAction } from "./auth.ts";
|
|
2
|
-
import type { ResultRecipient } from "./slice4.ts";
|
|
3
2
|
import { type DisplayCommon } from "./walletDisplayParts.ts";
|
|
3
|
+
import { type LinkedRecoverCreate, type LinkedRecoverDisplay } from "./linkedRecovery.ts";
|
|
4
|
+
import { type WalletCreateGrant, type WalletCreateGrantOutcome, type WalletCreateGrantWire } from "./walletCreateGrant.ts";
|
|
5
|
+
import { type ConsentDebitOutcome, type DebitTerms } from "./consentDebit.ts";
|
|
4
6
|
import { type LinkedSendDisplay, type LinkedWalletIdentity } from "./linkedSendDisplay.ts";
|
|
5
7
|
import { type MediatedDisplay, type MediatedPurgedDisplay } from "./mediatedOperation.ts";
|
|
6
8
|
export { LinkedSendAssets, MaxTransferCommentBytes, ReplaceActions, linkedSendAsset, replaceAction, transferComment } from "./linkedSendDisplay.ts";
|
|
7
9
|
export type { LinkedSendAsset, LinkedSendDisplay, LinkedWalletIdentity, ReplaceAction } from "./linkedSendDisplay.ts";
|
|
10
|
+
export type { LinkedRecoverCreate, LinkedRecoverDisplay } from "./linkedRecovery.ts";
|
|
8
11
|
export type { MediatedDisplay, MediatedLinkedSendDisplay, MediatedPurgedDisplay, MediatedSendDisplay } from "./mediatedOperation.ts";
|
|
9
12
|
export type UserWalletOperationKind = "wallet.create" | "wallet.sign" | "wallet.send" | "wallet.export" | "wallet.recover";
|
|
10
|
-
export type WalletResultKind = "signature" | "transfer_receipt" | "wallet_created";
|
|
13
|
+
export type WalletResultKind = "signature" | "transfer_receipt" | "wallet_created" | "device_share_recovered";
|
|
11
14
|
export type UserWalletState = "awaiting_confirmation" | "authorized" | "dispatching" | "outcome_unknown" | "succeeded" | "failed" | "denied" | "canceled" | "expired";
|
|
12
15
|
export type UserWalletEffectState = "no_effect" | "possible" | "known";
|
|
13
16
|
/**
|
|
@@ -20,10 +23,15 @@ export type UserWalletEffectState = "no_effect" | "possible" | "known";
|
|
|
20
23
|
* MediatedDisplay, with no result recipient.
|
|
21
24
|
*/
|
|
22
25
|
export type UserWalletAuthorizationBasis = "" | "user_confirmed" | "session_authorized" | "user_request_signed";
|
|
26
|
+
/**
|
|
27
|
+
* A wallet operation's result goes to its owner account only, so no create
|
|
28
|
+
* body names a result recipient: the device share of a created, exported or
|
|
29
|
+
* recovered wallet reaches this device alone, and the server refuses a
|
|
30
|
+
* service named as the recipient with INVALID_REQUEST.
|
|
31
|
+
*/
|
|
23
32
|
type UserWalletCommon = {
|
|
24
33
|
owner_type: "account";
|
|
25
34
|
owner_id: string;
|
|
26
|
-
result_recipient?: ResultRecipient;
|
|
27
35
|
};
|
|
28
36
|
/**
|
|
29
37
|
* UserWalletOperationCreate is the closed request body of source §2.2: the
|
|
@@ -36,6 +44,7 @@ export type UserWalletOperationCreate = (UserWalletCommon & {
|
|
|
36
44
|
network: string;
|
|
37
45
|
recipient_public_key: string;
|
|
38
46
|
};
|
|
47
|
+
grant?: WalletCreateGrantWire;
|
|
39
48
|
}) | (UserWalletCommon & {
|
|
40
49
|
kind: "wallet.sign";
|
|
41
50
|
wallet_id: string;
|
|
@@ -66,7 +75,7 @@ export type UserWalletOperationCreate = (UserWalletCommon & {
|
|
|
66
75
|
expected_generation: string;
|
|
67
76
|
recipient_public_key: string;
|
|
68
77
|
};
|
|
69
|
-
});
|
|
78
|
+
}) | LinkedRecoverCreate;
|
|
70
79
|
/** UserWalletPurgedDisplay is the safe identity left after material retention expires. */
|
|
71
80
|
export type UserWalletPurgedDisplay = DisplayCommon & {
|
|
72
81
|
kind: UserWalletOperationKind;
|
|
@@ -83,6 +92,8 @@ export type UserWalletDisplay = UserWalletPurgedDisplay | (DisplayCommon & {
|
|
|
83
92
|
network: string;
|
|
84
93
|
recipientPublicKey: string;
|
|
85
94
|
preparedSummary?: string;
|
|
95
|
+
grant?: WalletCreateGrant;
|
|
96
|
+
debit?: DebitTerms;
|
|
86
97
|
}) | (DisplayCommon & {
|
|
87
98
|
kind: "wallet.sign";
|
|
88
99
|
network: string;
|
|
@@ -105,7 +116,7 @@ export type UserWalletDisplay = UserWalletPurgedDisplay | (DisplayCommon & {
|
|
|
105
116
|
recipientPublicKey: string;
|
|
106
117
|
expectedGeneration: string;
|
|
107
118
|
preparedSummary?: string;
|
|
108
|
-
}) | MediatedDisplay;
|
|
119
|
+
}) | LinkedRecoverDisplay | MediatedDisplay;
|
|
109
120
|
/** walletMaterialPurged separates the purged display from every material display. */
|
|
110
121
|
export declare function walletMaterialPurged(display: UserWalletDisplay): display is UserWalletPurgedDisplay | MediatedPurgedDisplay;
|
|
111
122
|
export interface UserWalletOperation {
|
|
@@ -163,8 +174,14 @@ export interface WalletResult {
|
|
|
163
174
|
outputBase64url?: string;
|
|
164
175
|
/** Present exactly for a wallet_created result. */
|
|
165
176
|
wallet?: CreatedWallet;
|
|
166
|
-
/** Present exactly for a wallet_created result. */
|
|
177
|
+
/** Present exactly for a wallet_created and a device_share_recovered result. */
|
|
167
178
|
deviceShare?: DeviceSharePackage;
|
|
179
|
+
/** Present exactly for a device_share_recovered result: the linked wallet whose share was recovered. */
|
|
180
|
+
linkedWalletId?: string;
|
|
181
|
+
/** Present for a wallet_created result whose creation named a grant (walletCreateGrant.ts). */
|
|
182
|
+
grant?: WalletCreateGrantOutcome;
|
|
183
|
+
/** Present when the grant's service holds wallet:debit:user: the debit permission the creation's passkey also created (consentDebit.ts). */
|
|
184
|
+
debit?: ConsentDebitOutcome;
|
|
168
185
|
};
|
|
169
186
|
}
|
|
170
187
|
/** MaxWalletMessageBytes is the decoded wallet.sign payload bound of source §2.8. */
|
package/dist/walletOperations.js
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// authority. Local faults raise ClientFlowError; server denials stay
|
|
6
6
|
// AuthorizationError and are produced by the caller's transport.
|
|
7
7
|
import { ClientFlowError, canonicalDecimal, deadlineAfter, isNextAction, opaqueId, record, rfc3339UTC, } from "./auth.js";
|
|
8
|
-
import { bindingRevision, displayCommon, lowerUUID, rawQuantity, summaryOf, walletRecipient } from "./walletDisplayParts.js";
|
|
8
|
+
import { bindingRevision, displayCommon, lowerHexBytes, lowerUUID, rawQuantity, summaryOf, walletRecipient } from "./walletDisplayParts.js";
|
|
9
|
+
import { parseLinkedRecoverDisplay, parseRecoveredShareResult, validLinkedRecoverCreate } from "./linkedRecovery.js";
|
|
10
|
+
import { parseWalletCreateGrant, parseWalletCreateGrantOutcome, validWalletCreateGrant } from "./walletCreateGrant.js";
|
|
11
|
+
import { parseConsentDebitOutcome, parseDebitTerms } from "./consentDebit.js";
|
|
9
12
|
import { parseLinkedSendDisplay } from "./linkedSendDisplay.js";
|
|
10
13
|
import { parseMediatedOperation } from "./mediatedOperation.js";
|
|
11
14
|
export { LinkedSendAssets, MaxTransferCommentBytes, ReplaceActions, linkedSendAsset, replaceAction, transferComment } from "./linkedSendDisplay.js";
|
|
@@ -28,14 +31,17 @@ const base64urlValue = /^[A-Za-z0-9_-]+$/;
|
|
|
28
31
|
* caller sends the same closed body it validated.
|
|
29
32
|
*/
|
|
30
33
|
export function validateUserWalletCreate(value, expectedOwnerId) {
|
|
31
|
-
const common = record(value, ["kind", "owner_type", "owner_id", "payload"], ["wallet_id", "
|
|
32
|
-
|
|
34
|
+
const common = record(value, ["kind", "owner_type", "owner_id", "payload"], ["wallet_id", "linked_wallet_id", "grant"]);
|
|
35
|
+
// A grant is named beside a creation alone.
|
|
36
|
+
if (common.owner_type !== "account" || !opaqueId(common.owner_id) || common.owner_id !== expectedOwnerId ||
|
|
37
|
+
(common.grant !== undefined && (common.kind !== "wallet.create" || !validWalletCreateGrant(common.grant)))) {
|
|
33
38
|
throw new ClientFlowError("invalid_operation_create");
|
|
34
39
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
40
|
+
// A linked wallet is named by a recovery of its device share alone (linkedRecovery.ts).
|
|
41
|
+
if (common.linked_wallet_id !== undefined) {
|
|
42
|
+
if (!validLinkedRecoverCreate(common))
|
|
38
43
|
throw new ClientFlowError("invalid_operation_create");
|
|
44
|
+
return value;
|
|
39
45
|
}
|
|
40
46
|
const walletRequired = common.kind !== "wallet.create";
|
|
41
47
|
if (walletRequired !== opaqueId(common.wallet_id))
|
|
@@ -101,17 +107,19 @@ export function parseUserWalletOperation(payload) {
|
|
|
101
107
|
(root.result_kind !== undefined && !walletResultKind(root.result_kind)) || !isNextAction(root.next_action)) {
|
|
102
108
|
throw new ClientFlowError("invalid_server_response");
|
|
103
109
|
}
|
|
104
|
-
// A linked wallet is named by all three of its members, only for a send
|
|
105
|
-
//
|
|
110
|
+
// A linked wallet is named by all three of its members, only for a send or
|
|
111
|
+
// the recovery of its device share, never beside an own wallet; only a send
|
|
112
|
+
// is authorized by the session alone.
|
|
106
113
|
const linked = root.binding_source !== undefined;
|
|
107
|
-
if (linked && (root.binding_source !== "linked_wallet" || root.kind !== "wallet.send" || root.wallet_id !== undefined ||
|
|
108
|
-
!lowerUUID(root.connection_id) || !lowerUUID(root.linked_wallet_id) || !bindingRevision(root.binding_revision)
|
|
114
|
+
if (linked && (root.binding_source !== "linked_wallet" || (root.kind !== "wallet.send" && root.kind !== "wallet.recover") || root.wallet_id !== undefined ||
|
|
115
|
+
!lowerUUID(root.connection_id) || !lowerUUID(root.linked_wallet_id) || !bindingRevision(root.binding_revision) ||
|
|
116
|
+
(root.kind === "wallet.recover" && root.authorization_basis === "session_authorized"))) {
|
|
109
117
|
throw new ClientFlowError("invalid_server_response");
|
|
110
118
|
}
|
|
111
119
|
if (!linked && (root.connection_id !== undefined || root.linked_wallet_id !== undefined || root.binding_revision !== undefined || root.authorization_basis === "session_authorized")) {
|
|
112
120
|
throw new ClientFlowError("invalid_server_response");
|
|
113
121
|
}
|
|
114
|
-
const display = linked && !purgedDisplay(root.display) ? parseLinkedSendDisplay(root.display) : parseUserWalletDisplay(root.display, root.kind);
|
|
122
|
+
const display = linked && !purgedDisplay(root.display) ? (root.kind === "wallet.recover" ? parseLinkedRecoverDisplay(root.display) : parseLinkedSendDisplay(root.display)) : parseUserWalletDisplay(root.display, root.kind);
|
|
115
123
|
if (display.ownerId !== root.owner_id || display.requesterId !== root.requester_id || display.walletId !== root.wallet_id) {
|
|
116
124
|
throw new ClientFlowError("invalid_server_response");
|
|
117
125
|
}
|
|
@@ -153,12 +161,14 @@ export function parseUserWalletOperation(payload) {
|
|
|
153
161
|
*/
|
|
154
162
|
export function parseWalletResult(payload, expectedOperationId) {
|
|
155
163
|
const root = record(payload, ["request_id", "result"]);
|
|
156
|
-
|
|
164
|
+
const resultKind = root.result !== null && typeof root.result === "object" ? root.result.kind : undefined;
|
|
165
|
+
if (resultKind === "wallet_created")
|
|
157
166
|
return parseCreatedWalletResult(root.request_id, root.result, expectedOperationId);
|
|
158
|
-
|
|
167
|
+
if (resultKind === "device_share_recovered")
|
|
168
|
+
return parseRecoveredShareResult(root.request_id, root.result, expectedOperationId);
|
|
159
169
|
const result = record(root.result, ["operation_id", "kind", "receipt_id"], ["output_base64url"]);
|
|
160
170
|
if (!opaqueId(root.request_id) || !opaqueId(result.operation_id) || result.operation_id !== expectedOperationId ||
|
|
161
|
-
!walletResultKind(result.kind) || result.kind === "wallet_created" || !opaqueId(result.receipt_id)) {
|
|
171
|
+
!walletResultKind(result.kind) || result.kind === "wallet_created" || result.kind === "device_share_recovered" || !opaqueId(result.receipt_id)) {
|
|
162
172
|
throw new ClientFlowError("invalid_server_response");
|
|
163
173
|
}
|
|
164
174
|
if (result.kind === "signature") {
|
|
@@ -171,7 +181,7 @@ export function parseWalletResult(payload, expectedOperationId) {
|
|
|
171
181
|
return { requestId: root.request_id, result: { operationId: result.operation_id, kind: result.kind, receiptId: result.receipt_id } };
|
|
172
182
|
}
|
|
173
183
|
function parseCreatedWalletResult(requestId, value, expectedOperationId) {
|
|
174
|
-
const result = record(value, ["operation_id", "kind", "receipt_id", "wallet", "device_share"]);
|
|
184
|
+
const result = record(value, ["operation_id", "kind", "receipt_id", "wallet", "device_share"], ["grant", "debit"]);
|
|
175
185
|
const wallet = record(result.wallet, ["wallet_id", "key_id", "network", "address", "algorithm", "public_key"]);
|
|
176
186
|
const share = record(result.device_share, ["ciphertext", "nonce", "ephemeral_public_key", "encryption_algorithm", "encryption_context"]);
|
|
177
187
|
// The receipt of a creation is the created wallet ID; a result naming two
|
|
@@ -189,6 +199,8 @@ function parseCreatedWalletResult(requestId, value, expectedOperationId) {
|
|
|
189
199
|
operationId: expectedOperationId, kind: "wallet_created", receiptId: wallet.wallet_id,
|
|
190
200
|
wallet: { walletId: wallet.wallet_id, keyId: wallet.key_id, network: wallet.network, address: wallet.address, algorithm: wallet.algorithm, publicKey: wallet.public_key },
|
|
191
201
|
deviceShare: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeralPublicKey: share.ephemeral_public_key, encryptionAlgorithm: share.encryption_algorithm, encryptionContext: share.encryption_context },
|
|
202
|
+
...(result.grant === undefined ? {} : { grant: parseWalletCreateGrantOutcome(result.grant) }),
|
|
203
|
+
...(result.debit === undefined ? {} : { debit: parseConsentDebitOutcome(result.debit) }),
|
|
192
204
|
},
|
|
193
205
|
};
|
|
194
206
|
}
|
|
@@ -206,10 +218,11 @@ function parseUserWalletDisplay(value, kind) {
|
|
|
206
218
|
}
|
|
207
219
|
switch (kind) {
|
|
208
220
|
case "wallet.create": {
|
|
209
|
-
const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "network", "recipient_public_key"], ["wallet_id", "prepared_summary"]);
|
|
221
|
+
const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "network", "recipient_public_key"], ["wallet_id", "prepared_summary", "grant", "debit"]);
|
|
210
222
|
if (display.kind !== kind || !network(display.network) || !recipientPublicKey(display.recipient_public_key))
|
|
211
223
|
throw new ClientFlowError("invalid_server_response");
|
|
212
|
-
return { ...displayCommon(display), kind, network: display.network, recipientPublicKey: display.recipient_public_key, ...summaryOf(display.prepared_summary)
|
|
224
|
+
return { ...displayCommon(display), kind, network: display.network, recipientPublicKey: display.recipient_public_key, ...summaryOf(display.prepared_summary),
|
|
225
|
+
...(display.grant === undefined ? {} : { grant: parseWalletCreateGrant(display.grant) }), ...(display.debit === undefined ? {} : { debit: parseDebitTerms(display.debit) }) };
|
|
213
226
|
}
|
|
214
227
|
case "wallet.sign": {
|
|
215
228
|
const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "network", "message_base64url"], ["wallet_id", "prepared_summary"]);
|
|
@@ -273,11 +286,7 @@ function userWalletKind(value) {
|
|
|
273
286
|
return value === "wallet.create" || value === "wallet.sign" || value === "wallet.send" || value === "wallet.export" || value === "wallet.recover";
|
|
274
287
|
}
|
|
275
288
|
function walletResultKind(value) {
|
|
276
|
-
return value === "signature" || value === "transfer_receipt" || value === "wallet_created";
|
|
277
|
-
}
|
|
278
|
-
/** lowerHexBytes accepts 1..1024 bytes of lowercase hexadecimal. */
|
|
279
|
-
function lowerHexBytes(value) {
|
|
280
|
-
return typeof value === "string" && value.length >= 2 && value.length <= 2048 && value.length % 2 === 0 && /^[0-9a-f]+$/.test(value);
|
|
289
|
+
return value === "signature" || value === "transfer_receipt" || value === "wallet_created" || value === "device_share_recovered";
|
|
281
290
|
}
|
|
282
291
|
function algorithmName(value) {
|
|
283
292
|
return typeof value === "string" && /^[a-z0-9_-]{1,128}$/.test(value);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { FetchLike } from "./client.ts";
|
|
2
|
+
/** The code of the typed error that asks the application to start the top-level restore. */
|
|
3
|
+
export declare const webRenewalRequiresNavigation = "web_renewal_requires_navigation";
|
|
4
|
+
interface WebSessionTarget {
|
|
5
|
+
authorizationOrigin: string;
|
|
6
|
+
clientId: string;
|
|
7
|
+
/** The name the tabs of one application take turns under. */
|
|
8
|
+
lockName: string;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
/** Throws once the renewal or restore was superseded; asked under the lock right before each step that asks the provider. */
|
|
11
|
+
current?: () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface WebRenewalRequest extends WebSessionTarget {
|
|
14
|
+
fetch: FetchLike;
|
|
15
|
+
/** The session's CSRF token, held in memory only; without one the renewal starts from the cookie. */
|
|
16
|
+
csrfToken: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface WebRestoreRequest extends WebSessionTarget {
|
|
19
|
+
redirectUri: string;
|
|
20
|
+
crypto: Crypto;
|
|
21
|
+
/** The host's top-level launcher; it answers the registered redirect the restore came back to. */
|
|
22
|
+
launch: (url: string, signal?: AbortSignal) => Promise<string | void>;
|
|
23
|
+
/** POST /v1/auth/restore with this body, answering the session payload. */
|
|
24
|
+
exchange: (body: string) => Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The tabs of one application share the cookie, and the provider ends a
|
|
28
|
+
* session whose refresh value it is shown twice. Every renewal and every
|
|
29
|
+
* restore therefore runs under one Web Lock per client: each sends the value
|
|
30
|
+
* the previous one left. A restore holds it until the host's launcher
|
|
31
|
+
* settles or the restore's signal aborts it; the host gets the same signal
|
|
32
|
+
* and closes its window, so no abandoned navigation rotates the value later.
|
|
33
|
+
* A sign-in's exchange, which sets the cookie too, goes out under the same
|
|
34
|
+
* lock (client.ts), so a renewal's late answer never follows a newer
|
|
35
|
+
* sign-in's.
|
|
36
|
+
*/
|
|
37
|
+
export declare function underRenewalLock<T>(target: WebSessionTarget & {
|
|
38
|
+
whenWaiting?: () => void;
|
|
39
|
+
}, work: () => Promise<T>): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* renewWebSession is the background call: POST /v1/auth/renew with the cookie
|
|
42
|
+
* (credentials: "include"). A page that holds the session's CSRF token sends
|
|
43
|
+
* it; a page that holds none (a reload, a new tab), or whose token a renewal
|
|
44
|
+
* or restore in another tab replaced, renews from the cookie alone with the
|
|
45
|
+
* fixed X-Authorization-Renew header and gets a new token. It answers the
|
|
46
|
+
* session payload.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renewWebSession(request: WebRenewalRequest): Promise<unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* staleCsrfRefusal is the answer a web lifecycle call gets when a renewal or
|
|
51
|
+
* restore in another tab replaced the session's CSRF token since this page
|
|
52
|
+
* last renewed.
|
|
53
|
+
*/
|
|
54
|
+
export declare function staleCsrfRefusal(error: unknown): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* revokeWithCurrentCredentials is logout's way past a CSRF token another tab
|
|
57
|
+
* replaced. Under the tabs' lock, held until the revocation answers, it
|
|
58
|
+
* renews from the cookie alone and revokes with the tokens that answer, but
|
|
59
|
+
* only when they belong to the session the logout was asked for: a cookie
|
|
60
|
+
* another sign-in replaced is left alone and the revocation is not confirmed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function revokeWithCurrentCredentials(request: Omit<WebRenewalRequest, "csrfToken"> & {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
whenWaiting?: () => void;
|
|
65
|
+
}, revoke: (accessToken: string, csrfToken: string) => Promise<unknown>): Promise<unknown>;
|
|
66
|
+
/** revokedAnswer says a logout answer confirms the revocation. */
|
|
67
|
+
export declare function revokedAnswer(payload: unknown): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* revokeUnclaimedSession ends the session of a sign-in answer this page does
|
|
70
|
+
* not install, because a logout or a newer sign-in overtook it while it was
|
|
71
|
+
* asked for. The answer set the cookie, so without it a reload would take up
|
|
72
|
+
* a session nobody holds, one the person may have logged out of. The
|
|
73
|
+
* provider clears the cookie only while it holds that session. A CSRF token
|
|
74
|
+
* another tab replaced meanwhile (it renewed that session from the cookie) is
|
|
75
|
+
* followed by the logout's recovery: one renewal from the cookie under the
|
|
76
|
+
* tabs' lock, checked against the answer's session, and the revocation with
|
|
77
|
+
* its tokens. It answers true only for a confirmed revocation of exactly that
|
|
78
|
+
* session; a cookie that holds another session is left alone and answers
|
|
79
|
+
* false, as does any refusal or failure. whenWaiting is told when the
|
|
80
|
+
* recovery has to wait for the lock (a restore may hold it while its window
|
|
81
|
+
* is open): the revocation goes on, but nobody needs to wait for its verdict.
|
|
82
|
+
*/
|
|
83
|
+
export declare function revokeUnclaimedSession(request: Omit<WebRenewalRequest, "csrfToken"> & {
|
|
84
|
+
whenWaiting?: () => void;
|
|
85
|
+
}, payload: unknown, revoke: (accessToken: string, csrfToken: string) => Promise<unknown>): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* restoreWebSession is the top-level restore: a fresh state and S256 verifier
|
|
88
|
+
* in memory, the provider restore URL through the host's launcher, and the
|
|
89
|
+
* one-use handoff of the redirect it returns exchanged for the session.
|
|
90
|
+
*/
|
|
91
|
+
export declare function restoreWebSession(request: WebRestoreRequest): Promise<unknown>;
|
|
92
|
+
export {};
|
|
@@ -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,66 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shuttle.technology/authorization",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"homepage": "https://www.npmjs.com/package/@shuttle.technology/authorization",
|
|
6
|
-
"packageManager": "pnpm@11.19.0",
|
|
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.",
|
|
7
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
8
7
|
"files": [
|
|
9
8
|
"dist/**/*.js",
|
|
10
9
|
"dist/**/*.d.ts",
|
|
11
|
-
"README.md"
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
12
|
],
|
|
13
13
|
"main": "./dist/client.js",
|
|
14
14
|
"types": "./dist/client.d.ts",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
17
|
"types": "./dist/client.d.ts",
|
|
18
|
-
"
|
|
18
|
+
"default": "./dist/client.js"
|
|
19
19
|
},
|
|
20
20
|
"./mode-c": {
|
|
21
21
|
"types": "./dist/modeC.d.ts",
|
|
22
|
-
"
|
|
22
|
+
"default": "./dist/modeC.js"
|
|
23
23
|
},
|
|
24
24
|
"./mode-c-linked": {
|
|
25
25
|
"types": "./dist/modeCLinked.d.ts",
|
|
26
|
-
"
|
|
26
|
+
"default": "./dist/modeCLinked.js"
|
|
27
27
|
},
|
|
28
28
|
"./streams": {
|
|
29
29
|
"types": "./dist/streams.d.ts",
|
|
30
|
-
"
|
|
30
|
+
"default": "./dist/streams.js"
|
|
31
31
|
},
|
|
32
32
|
"./linked-wallets": {
|
|
33
33
|
"types": "./dist/linkedWallets.d.ts",
|
|
34
|
-
"
|
|
34
|
+
"default": "./dist/linkedWallets.js"
|
|
35
|
+
},
|
|
36
|
+
"./debit-permissions": {
|
|
37
|
+
"types": "./dist/debitPermissions.d.ts",
|
|
38
|
+
"default": "./dist/debitPermissions.js"
|
|
35
39
|
},
|
|
36
40
|
"./device-key": {
|
|
37
41
|
"types": "./dist/deviceKey.d.ts",
|
|
38
|
-
"
|
|
42
|
+
"default": "./dist/deviceKey.js"
|
|
39
43
|
},
|
|
40
44
|
"./device-share": {
|
|
41
45
|
"types": "./dist/deviceShare.d.ts",
|
|
42
|
-
"
|
|
46
|
+
"default": "./dist/deviceShare.js"
|
|
43
47
|
}
|
|
44
48
|
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
45
52
|
"publishConfig": {
|
|
46
53
|
"access": "public",
|
|
47
54
|
"registry": "https://registry.npmjs.org/"
|
|
48
55
|
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "24.13.3",
|
|
58
|
+
"typescript": "7.0.2"
|
|
59
|
+
},
|
|
49
60
|
"scripts": {
|
|
50
|
-
"check": "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/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/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/mediatedOperation.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/requestGrant.ts && node --experimental-strip-types --check src/sends.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/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 --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-demo.mjs",
|
|
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",
|
|
51
62
|
"typecheck": "tsc --noEmit",
|
|
52
63
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
53
|
-
"prepack": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
54
|
-
"package:smoke": "node test/package-smoke.mjs",
|
|
55
64
|
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
56
65
|
"integration:http": "node test/http-integration.mjs",
|
|
57
66
|
"verify": "pnpm run check && pnpm run typecheck && pnpm run build && pnpm run test"
|
|
58
|
-
},
|
|
59
|
-
"engines": {
|
|
60
|
-
"node": "24.19.0"
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@types/node": "24.13.3",
|
|
64
|
-
"typescript": "7.0.2"
|
|
65
67
|
}
|
|
66
|
-
}
|
|
68
|
+
}
|