@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
package/dist/linkedWallets.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type NextAction } from './auth.ts';
|
|
|
2
2
|
import { type WalletEventPage } from './streams.ts';
|
|
3
3
|
export { resumeCursor } from './streams.ts';
|
|
4
4
|
import { type LocalOperation, type ResultRecipient } from './slice4.ts';
|
|
5
|
+
export { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperation, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.ts';
|
|
5
6
|
import { type LinkedWalletSignalEnd, type LinkedWalletSignalHandlers } from './linkedWalletSignals.ts';
|
|
6
7
|
export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets, type LinkedWalletSignalEnd, type LinkedWalletSignalHandlers } from './linkedWalletSignals.ts';
|
|
7
8
|
import { type TransferStatus } from './transferStatus.ts';
|
|
@@ -14,6 +15,9 @@ import type { PageOptions } from './pages.ts';
|
|
|
14
15
|
export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates, type TransferState, type TransferStatus } from './transferStatus.ts';
|
|
15
16
|
import { type WalletLimits, type WalletLimitsInput } from './walletLimits.ts';
|
|
16
17
|
export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients, type LimitAsset, type WalletCeiling, type WalletLimitRefusal, type WalletLimits, type WalletLimitsDisplay, type WalletLimitsInput } from './walletLimits.ts';
|
|
18
|
+
import { DebitPermissions } from './debitPermissions.ts';
|
|
19
|
+
export { DebitPermissions, MaxDebitAssets, MaxDebitRecipients, type DebitAsset, type DebitAssetBound, type DebitPermission, type DebitPermissionDisplay, type DebitPermissionInput, type DebitPermissionList, type DebitPermissionRevokeDisplay, type DebitPermissionState, type DebitShareKeys } from './debitPermissions.ts';
|
|
20
|
+
import type { DeviceRecoveryOptions, LinkedDeviceShares } from './deviceShares.ts';
|
|
17
21
|
import { type LinkedSendAsset, type ReplaceAction, type DeviceSharePackage, type UserWalletOperation, type UserWalletState, type UserWalletEffectState, type UserWalletAuthorizationBasis } from './walletOperations.ts';
|
|
18
22
|
/** session_authorized: the account's session registered its own linked wallet; no confirmation exists. */
|
|
19
23
|
export type LinkedRegistrationAuthorizationBasis = UserWalletAuthorizationBasis;
|
|
@@ -76,7 +80,6 @@ export interface LinkedRegistrationCreate {
|
|
|
76
80
|
assets?: string[];
|
|
77
81
|
activity_types?: string[];
|
|
78
82
|
};
|
|
79
|
-
result_recipient?: ResultRecipient;
|
|
80
83
|
}
|
|
81
84
|
export interface LinkedRegistrationDisplay {
|
|
82
85
|
kind: 'streams.register';
|
|
@@ -154,7 +157,6 @@ export interface LinkedSendInput {
|
|
|
154
157
|
recipient: string;
|
|
155
158
|
amountRaw: string;
|
|
156
159
|
comment?: string;
|
|
157
|
-
resultRecipient?: ResultRecipient;
|
|
158
160
|
}
|
|
159
161
|
/**
|
|
160
162
|
* The replacement of a send the network has not included. operationId is the
|
|
@@ -165,7 +167,6 @@ export interface LinkedReplaceInput {
|
|
|
165
167
|
linkedWalletId: string;
|
|
166
168
|
operationId: string;
|
|
167
169
|
action: ReplaceAction;
|
|
168
|
-
resultRecipient?: ResultRecipient;
|
|
169
170
|
}
|
|
170
171
|
/** The own-wallet system's transport key, as that system publishes it. It carries no proof of where the key lives. */
|
|
171
172
|
export interface LinkedTransportKey {
|
|
@@ -218,6 +219,8 @@ export interface LinkedWalletTransport {
|
|
|
218
219
|
}>;
|
|
219
220
|
accountId(): string;
|
|
220
221
|
now(): number;
|
|
222
|
+
/** The device shares the client keeps (deviceShares.ts); absent, every call that asks the SDK for a share refuses with device_share_unavailable. */
|
|
223
|
+
devices?: LinkedDeviceShares;
|
|
221
224
|
}
|
|
222
225
|
export declare class LinkedWallets {
|
|
223
226
|
#private;
|
|
@@ -275,7 +278,6 @@ export declare class LinkedWallets {
|
|
|
275
278
|
network: string;
|
|
276
279
|
assets?: string[];
|
|
277
280
|
activityTypes?: string[];
|
|
278
|
-
resultRecipient?: ResultRecipient;
|
|
279
281
|
}, idempotencyKey: string, signal?: AbortSignal): Promise<LinkedRegistrationOperation>;
|
|
280
282
|
operation(operationId: string, signal?: AbortSignal): Promise<LinkedRegistrationOperation>;
|
|
281
283
|
executeRegistration(operationId: string, version: string, idempotencyKey: string, signal?: AbortSignal): Promise<LinkedRegistrationOperation>;
|
|
@@ -323,11 +325,45 @@ export declare class LinkedWallets {
|
|
|
323
325
|
transportKey(linkedWalletId: string, signal?: AbortSignal): Promise<LinkedTransportKey>;
|
|
324
326
|
/**
|
|
325
327
|
* Executes a prepared send, on the owner's confirm, with the device share
|
|
326
|
-
* resealed for the own-wallet system
|
|
327
|
-
*
|
|
328
|
-
*
|
|
328
|
+
* resealed for the own-wallet system. Without deviceShare the SDK supplies
|
|
329
|
+
* the share it keeps (deviceShareFor()), recovered first with one passkey
|
|
330
|
+
* confirmation when this browser holds none; an app that keeps its own share
|
|
331
|
+
* passes it resealed (resealDeviceShare). The provider cannot open the share.
|
|
332
|
+
* A send never prepared is refused with VERSION_CONFLICT, an expired one
|
|
333
|
+
* with OPERATION_EXPIRED.
|
|
329
334
|
*/
|
|
330
|
-
executeSend(operationId: string, version: string, idempotencyKey: string, deviceShare
|
|
335
|
+
executeSend(operationId: string, version: string, idempotencyKey: string, deviceShare?: DeviceSharePackage, signal?: AbortSignal): Promise<UserWalletOperation>;
|
|
336
|
+
/**
|
|
337
|
+
* The device share the SDK keeps for this wallet, resealed to the
|
|
338
|
+
* own-wallet system's transport key read right before: what executeSend()
|
|
339
|
+
* sends and what a Mode C request is signed with (signLinkedMediatedRequest).
|
|
340
|
+
* A browser that holds no share of the wallet (another domain, cleared site
|
|
341
|
+
* data, another browser or device) recovers it first: the person confirms
|
|
342
|
+
* once with the passkey on the provider's page, and the share the wallet had
|
|
343
|
+
* elsewhere stops working. Two tabs never recover the same wallet at once.
|
|
344
|
+
*/
|
|
345
|
+
deviceShareFor(linkedWalletId: string, signal?: AbortSignal): Promise<DeviceSharePackage>;
|
|
346
|
+
/**
|
|
347
|
+
* The device key of the wallet's latest creation or recovery the provider
|
|
348
|
+
* made, as 33-byte compressed hex; absent when it knows none. A share kept
|
|
349
|
+
* here that is sealed to another key was replaced by a recovery elsewhere.
|
|
350
|
+
*/
|
|
351
|
+
deviceKey(linkedWalletId: string, signal?: AbortSignal): Promise<{
|
|
352
|
+
requestId: string;
|
|
353
|
+
devicePublicKey?: string;
|
|
354
|
+
}>;
|
|
355
|
+
/** Whether this browser holds a current device share of the wallet; without one the next send recovers it with the passkey. */
|
|
356
|
+
hasDeviceShare(linkedWalletId: string, signal?: AbortSignal): Promise<boolean>;
|
|
357
|
+
/**
|
|
358
|
+
* Recovers the wallet's device share onto this browser when it holds no
|
|
359
|
+
* current one, as deviceShareFor() would, for an app that does it ahead of a
|
|
360
|
+
* transfer; force recovers even then. credentialId names the passkey that
|
|
361
|
+
* confirms it, by default the account's newest. Answers whether a recovery
|
|
362
|
+
* ran. Optional: sends recover by themselves.
|
|
363
|
+
*/
|
|
364
|
+
recoverDevice(linkedWalletId: string, options?: DeviceRecoveryOptions & {
|
|
365
|
+
force?: boolean;
|
|
366
|
+
}): Promise<boolean>;
|
|
331
367
|
/**
|
|
332
368
|
* What became of the transfer a send prepared: on its way, confirmed by the
|
|
333
369
|
* network, or failed and why. The send's operation succeeds when the
|
|
@@ -352,6 +388,8 @@ export declare class LinkedWallets {
|
|
|
352
388
|
* executeOperation() apply it. A send the limits refuse answers one of WalletLimitRefusals.
|
|
353
389
|
*/
|
|
354
390
|
setLimits(linkedWalletId: string, limits: WalletLimitsInput, idempotencyKey: string, signal?: AbortSignal): Promise<LocalOperation>;
|
|
391
|
+
/** The permissions the account lets services debit its linked wallets by, without the device (debitPermissions.ts). */
|
|
392
|
+
debitPermissions(): DebitPermissions;
|
|
355
393
|
}
|
|
356
394
|
export declare function parseWalletConnection(payload: unknown): WalletConnection;
|
|
357
395
|
export declare function parseLinkedWallet(payload: unknown): LinkedWallet;
|
|
@@ -361,13 +399,3 @@ export declare function parseLinkedWalletList(payload: unknown): {
|
|
|
361
399
|
};
|
|
362
400
|
export declare function parseWalletLinkChallenge(payload: unknown, now: number): WalletLinkChallenge;
|
|
363
401
|
export declare function validateWalletLinkAssertion(value: WalletLinkAssertion): WalletLinkAssertion;
|
|
364
|
-
export declare function validateLinkedRegistrationCreate(value: LinkedRegistrationCreate, ownerId: string): LinkedRegistrationCreate;
|
|
365
|
-
export declare function parseLinkedRegistrationOperationEnvelope(payload: unknown): {
|
|
366
|
-
requestId: string;
|
|
367
|
-
operation: LinkedRegistrationOperation;
|
|
368
|
-
};
|
|
369
|
-
export declare function parseLinkedRegistrationOperation(payload: unknown): LinkedRegistrationOperation;
|
|
370
|
-
export declare function parseLinkedRegistrationResult(payload: unknown, operationId: string): LinkedRegistrationResult;
|
|
371
|
-
export declare function linkedRegistrationsPath(linkedWalletId: string, options?: LinkedRegistrationListOptions): string;
|
|
372
|
-
export declare function linkedEventsPath(linkedWalletId: string, options?: LinkedEventsOptions): string;
|
|
373
|
-
export declare function parseLinkedStreamsRegistrationList(payload: unknown, linkedWalletId: string): LinkedStreamsRegistrationList;
|
package/dist/linkedWallets.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
// shown, executeSend() sends it on the owner's confirm). No passkey is asked
|
|
9
9
|
// for either (owner decisions of 8 and 14 October 2026); a passkey still
|
|
10
10
|
// confirms creating a wallet.
|
|
11
|
-
import { ClientFlowError, canonicalDecimal,
|
|
12
|
-
import {
|
|
11
|
+
import { ClientFlowError, canonicalDecimal, integer, opaqueId, record, rfc3339UTC, unexpiredRFC3339 } from './auth.js';
|
|
12
|
+
import { parseWalletEventPage } from './streams.js';
|
|
13
13
|
export { resumeCursor } from './streams.js';
|
|
14
14
|
import { parseOperationEnvelope } from './slice4.js';
|
|
15
|
+
import { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.js';
|
|
16
|
+
export { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperation, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.js';
|
|
15
17
|
import { linkedSignalsPath, readLinkedWalletSignals } from './linkedWalletSignals.js';
|
|
16
18
|
export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets } from './linkedWalletSignals.js';
|
|
17
19
|
import { parseTransferStatus, transferStatusPath } from './transferStatus.js';
|
|
@@ -22,7 +24,9 @@ export { eventOfTransaction, parseSendPage, sendEvents, sendsPath } from './send
|
|
|
22
24
|
export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates } from './transferStatus.js';
|
|
23
25
|
import { parseWalletLimits, walletLimitsCreate } from './walletLimits.js';
|
|
24
26
|
export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients } from './walletLimits.js';
|
|
25
|
-
import {
|
|
27
|
+
import { DebitPermissions } from './debitPermissions.js';
|
|
28
|
+
export { DebitPermissions, MaxDebitAssets, MaxDebitRecipients } from './debitPermissions.js';
|
|
29
|
+
import { b64, fault, hex, network, publicKey, requestID, requestUUID, safeRevision, text, token, uuid } from './linkedChecks.js';
|
|
26
30
|
import { linkedSendAsset, parseUserWalletOperationEnvelope, replaceAction, transferComment } from './walletOperations.js';
|
|
27
31
|
export class LinkedWallets {
|
|
28
32
|
#transport;
|
|
@@ -140,15 +144,13 @@ export class LinkedWallets {
|
|
|
140
144
|
* executeRegistration() with its version dispatches it.
|
|
141
145
|
*/
|
|
142
146
|
register(input, idempotencyKey, signal) {
|
|
143
|
-
record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes'
|
|
147
|
+
record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes']);
|
|
144
148
|
const create = { kind: 'streams.register', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: input.linkedWalletId, payload: { network: input.network } };
|
|
145
149
|
if (input.assets !== undefined)
|
|
146
150
|
create.payload.assets = input.assets;
|
|
147
151
|
// An empty selection means deposits only and is not sent; any other value is checked with the whole request.
|
|
148
152
|
if (input.activityTypes !== undefined && !(Array.isArray(input.activityTypes) && input.activityTypes.length === 0))
|
|
149
153
|
create.payload.activity_types = input.activityTypes;
|
|
150
|
-
if (input.resultRecipient !== undefined)
|
|
151
|
-
create.result_recipient = input.resultRecipient;
|
|
152
154
|
return this.createRegistration(create, idempotencyKey, signal);
|
|
153
155
|
}
|
|
154
156
|
async operation(operationId, signal) {
|
|
@@ -181,7 +183,7 @@ export class LinkedWallets {
|
|
|
181
183
|
* executeSend() with the device share.
|
|
182
184
|
*/
|
|
183
185
|
async send(input, idempotencyKey, signal) {
|
|
184
|
-
const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment'
|
|
186
|
+
const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment']);
|
|
185
187
|
requestID(idempotencyKey);
|
|
186
188
|
if (!uuid(declared.linkedWalletId) || !network(declared.network) || !linkedSendAsset(declared.asset) || typeof declared.recipient !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(declared.recipient) ||
|
|
187
189
|
!canonicalDecimal(declared.amountRaw) || declared.amountRaw === '0' || declared.amountRaw.length > 78 ||
|
|
@@ -192,8 +194,6 @@ export class LinkedWallets {
|
|
|
192
194
|
if (declared.comment !== undefined)
|
|
193
195
|
sent.comment = declared.comment;
|
|
194
196
|
const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId, payload: sent };
|
|
195
|
-
if (declared.resultRecipient !== undefined)
|
|
196
|
-
create.result_recipient = recipient(declared.resultRecipient);
|
|
197
197
|
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
198
198
|
const operation = parseUserWalletOperationEnvelope(payload).operation;
|
|
199
199
|
const shown = operation.display;
|
|
@@ -214,14 +214,12 @@ export class LinkedWallets {
|
|
|
214
214
|
* transferStatus() of both.
|
|
215
215
|
*/
|
|
216
216
|
async replaceSend(input, idempotencyKey, signal) {
|
|
217
|
-
const declared = record(input, ['linkedWalletId', 'operationId', 'action']
|
|
217
|
+
const declared = record(input, ['linkedWalletId', 'operationId', 'action']);
|
|
218
218
|
requestID(idempotencyKey);
|
|
219
219
|
if (!uuid(declared.linkedWalletId) || !opaqueId(declared.operationId) || !replaceAction(declared.action))
|
|
220
220
|
throw new ClientFlowError('invalid_operation_create');
|
|
221
221
|
const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId,
|
|
222
222
|
payload: { action: declared.action, replaces_operation_id: declared.operationId } };
|
|
223
|
-
if (declared.resultRecipient !== undefined)
|
|
224
|
-
create.result_recipient = recipient(declared.resultRecipient);
|
|
225
223
|
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
226
224
|
const operation = parseUserWalletOperationEnvelope(payload).operation;
|
|
227
225
|
const shown = operation.display;
|
|
@@ -267,15 +265,26 @@ export class LinkedWallets {
|
|
|
267
265
|
}
|
|
268
266
|
/**
|
|
269
267
|
* Executes a prepared send, on the owner's confirm, with the device share
|
|
270
|
-
* resealed for the own-wallet system
|
|
271
|
-
*
|
|
272
|
-
*
|
|
268
|
+
* resealed for the own-wallet system. Without deviceShare the SDK supplies
|
|
269
|
+
* the share it keeps (deviceShareFor()), recovered first with one passkey
|
|
270
|
+
* confirmation when this browser holds none; an app that keeps its own share
|
|
271
|
+
* passes it resealed (resealDeviceShare). The provider cannot open the share.
|
|
272
|
+
* A send never prepared is refused with VERSION_CONFLICT, an expired one
|
|
273
|
+
* with OPERATION_EXPIRED.
|
|
273
274
|
*/
|
|
274
275
|
async executeSend(operationId, version, idempotencyKey, deviceShare, signal) {
|
|
275
276
|
requestID(operationId);
|
|
276
277
|
requestID(idempotencyKey);
|
|
278
|
+
if (!canonicalDecimal(version))
|
|
279
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
280
|
+
if (deviceShare === undefined) {
|
|
281
|
+
const linkedWalletId = parseUserWalletOperationEnvelope(await this.#transport.get(`/v1/operations/${operationId}`, signal)).operation.linkedWallet?.linkedWalletId;
|
|
282
|
+
if (linkedWalletId === undefined)
|
|
283
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
284
|
+
deviceShare = await this.deviceShareFor(linkedWalletId, signal);
|
|
285
|
+
}
|
|
277
286
|
const share = record(deviceShare, ['ciphertext', 'nonce', 'ephemeralPublicKey', 'encryptionAlgorithm', 'encryptionContext']);
|
|
278
|
-
if (!
|
|
287
|
+
if (![share.ciphertext, share.nonce, share.ephemeralPublicKey, share.encryptionContext].every(hex) || typeof share.encryptionAlgorithm !== 'string')
|
|
279
288
|
throw new ClientFlowError('invalid_execute_request');
|
|
280
289
|
const body = { device_share: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeral_public_key: share.ephemeralPublicKey, encryption_algorithm: share.encryptionAlgorithm, encryption_context: share.encryptionContext } };
|
|
281
290
|
const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', body, signal, idempotencyKey, version)).payload).operation;
|
|
@@ -283,6 +292,64 @@ export class LinkedWallets {
|
|
|
283
292
|
fault();
|
|
284
293
|
return operation;
|
|
285
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The device share the SDK keeps for this wallet, resealed to the
|
|
297
|
+
* own-wallet system's transport key read right before: what executeSend()
|
|
298
|
+
* sends and what a Mode C request is signed with (signLinkedMediatedRequest).
|
|
299
|
+
* A browser that holds no share of the wallet (another domain, cleared site
|
|
300
|
+
* data, another browser or device) recovers it first: the person confirms
|
|
301
|
+
* once with the passkey on the provider's page, and the share the wallet had
|
|
302
|
+
* elsewhere stops working. Two tabs never recover the same wallet at once.
|
|
303
|
+
*/
|
|
304
|
+
async deviceShareFor(linkedWalletId, signal) {
|
|
305
|
+
return this.#sealedTo(linkedWalletId, async () => (await this.transportKey(linkedWalletId, signal)).publicKey, signal);
|
|
306
|
+
}
|
|
307
|
+
/** The kept share, recovered first when this browser holds none (with recover false: refused), resealed to the key read right before. */
|
|
308
|
+
async #sealedTo(linkedWalletId, publicKey, signal, recover = true) {
|
|
309
|
+
const devices = this.#devices();
|
|
310
|
+
const { wallet } = await this.wallet(linkedWalletId, signal);
|
|
311
|
+
if (recover)
|
|
312
|
+
await devices.ensure(wallet, this.#serverKey(linkedWalletId, signal), { signal });
|
|
313
|
+
else if (!(await devices.has(wallet, this.#serverKey(linkedWalletId, signal))))
|
|
314
|
+
throw new ClientFlowError('device_share_unavailable');
|
|
315
|
+
return devices.sealedFor(wallet, await publicKey());
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The device key of the wallet's latest creation or recovery the provider
|
|
319
|
+
* made, as 33-byte compressed hex; absent when it knows none. A share kept
|
|
320
|
+
* here that is sealed to another key was replaced by a recovery elsewhere.
|
|
321
|
+
*/
|
|
322
|
+
async deviceKey(linkedWalletId, signal) {
|
|
323
|
+
requestUUID(linkedWalletId);
|
|
324
|
+
const r = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/device-key`, signal), ['request_id'], ['device_public_key']);
|
|
325
|
+
if (!opaqueId(r.request_id) || (r.device_public_key !== undefined && (typeof r.device_public_key !== 'string' || !/^0[23][0-9a-f]{64}$/.test(r.device_public_key))))
|
|
326
|
+
fault();
|
|
327
|
+
return r.device_public_key === undefined ? { requestId: r.request_id } : { requestId: r.request_id, devicePublicKey: r.device_public_key };
|
|
328
|
+
}
|
|
329
|
+
/** Whether this browser holds a current device share of the wallet; without one the next send recovers it with the passkey. */
|
|
330
|
+
async hasDeviceShare(linkedWalletId, signal) {
|
|
331
|
+
const devices = this.#devices();
|
|
332
|
+
return devices.has((await this.wallet(linkedWalletId, signal)).wallet, this.#serverKey(linkedWalletId, signal));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Recovers the wallet's device share onto this browser when it holds no
|
|
336
|
+
* current one, as deviceShareFor() would, for an app that does it ahead of a
|
|
337
|
+
* transfer; force recovers even then. credentialId names the passkey that
|
|
338
|
+
* confirms it, by default the account's newest. Answers whether a recovery
|
|
339
|
+
* ran. Optional: sends recover by themselves.
|
|
340
|
+
*/
|
|
341
|
+
async recoverDevice(linkedWalletId, options = {}) {
|
|
342
|
+
const devices = this.#devices();
|
|
343
|
+
return devices.ensure((await this.wallet(linkedWalletId, options.signal)).wallet, this.#serverKey(linkedWalletId, options.signal), options);
|
|
344
|
+
}
|
|
345
|
+
#serverKey(linkedWalletId, signal) {
|
|
346
|
+
return async () => (await this.deviceKey(linkedWalletId, signal)).devicePublicKey;
|
|
347
|
+
}
|
|
348
|
+
#devices() {
|
|
349
|
+
if (!this.#transport.devices)
|
|
350
|
+
throw new ClientFlowError('device_share_unavailable');
|
|
351
|
+
return this.#transport.devices;
|
|
352
|
+
}
|
|
286
353
|
/**
|
|
287
354
|
* What became of the transfer a send prepared: on its way, confirmed by the
|
|
288
355
|
* network, or failed and why. The send's operation succeeds when the
|
|
@@ -323,6 +390,8 @@ export class LinkedWallets {
|
|
|
323
390
|
fault();
|
|
324
391
|
return operation;
|
|
325
392
|
}
|
|
393
|
+
/** The permissions the account lets services debit its linked wallets by, without the device (debitPermissions.ts). */
|
|
394
|
+
debitPermissions() { return new DebitPermissions(this.#transport, (linkedWalletId, keeperKey, signal, recover) => this.#sealedTo(linkedWalletId, async () => keeperKey, signal, recover)); }
|
|
326
395
|
}
|
|
327
396
|
export function parseWalletConnection(payload) {
|
|
328
397
|
const r = record(payload, ['request_id', 'id', 'connector_id', 'state', 'created_at', 'last_synced_at'], ['revoked_at']);
|
|
@@ -365,107 +434,3 @@ export function validateWalletLinkAssertion(value) {
|
|
|
365
434
|
throw new ClientFlowError('invalid_linked_wallet_request');
|
|
366
435
|
return value;
|
|
367
436
|
}
|
|
368
|
-
export function validateLinkedRegistrationCreate(value, ownerId) {
|
|
369
|
-
const r = record(value, ['kind', 'owner_type', 'owner_id', 'linked_wallet_id', 'payload'], ['result_recipient']);
|
|
370
|
-
const p = record(r.payload, ['network'], ['assets', 'activity_types']);
|
|
371
|
-
if (r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || r.owner_id !== ownerId || !uuid(r.linked_wallet_id) || !network(p.network) || (p.assets !== undefined && !assets(p.assets)) || (p.activity_types !== undefined && !activityTypes(p.activity_types)))
|
|
372
|
-
throw new ClientFlowError('invalid_operation_create');
|
|
373
|
-
if (r.result_recipient !== undefined)
|
|
374
|
-
recipient(r.result_recipient);
|
|
375
|
-
return value;
|
|
376
|
-
}
|
|
377
|
-
export function parseLinkedRegistrationOperationEnvelope(payload) {
|
|
378
|
-
const r = record(payload, ['request_id', 'operation']);
|
|
379
|
-
if (!opaqueId(r.request_id))
|
|
380
|
-
fault();
|
|
381
|
-
return { requestId: r.request_id, operation: parseLinkedRegistrationOperation(r.operation) };
|
|
382
|
-
}
|
|
383
|
-
export function parseLinkedRegistrationOperation(payload) {
|
|
384
|
-
const r = record(payload, ['id', 'version', 'kind', 'state', 'effect_state', 'authorization_basis', 'owner_type', 'owner_id', 'requester_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'request_fingerprint', 'operation_fingerprint', 'display', 'created_at', 'expires_at', 'next_action'], ['confirmation_location']);
|
|
385
|
-
if (!opaqueId(r.id) || !canonicalDecimal(r.version) || r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || !opaqueId(r.requester_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || !safeRevision(r.binding_revision) || !digest(r.request_fingerprint) || !digest(r.operation_fingerprint) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.expires_at) || !deadlineAfter(r.created_at, r.expires_at) || !isNextAction(r.next_action) || !['awaiting_confirmation', 'authorized', 'dispatching', 'outcome_unknown', 'succeeded', 'failed', 'denied', 'canceled', 'expired'].includes(r.state) || !['no_effect', 'possible', 'known'].includes(r.effect_state) || !['', 'user_confirmed', 'session_authorized'].includes(r.authorization_basis) || (r.confirmation_location !== undefined && !https(r.confirmation_location)))
|
|
386
|
-
fault();
|
|
387
|
-
const purged = typeof r.display === 'object' && r.display !== null && 'material_purged' in r.display;
|
|
388
|
-
const d = record(r.display, ['kind', 'owner_id', 'requester_id', 'result_recipient', ...(purged ? ['material_purged'] : ['binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'assets', 'network', 'address', 'streams_mapping_id', 'streams_mapping_version', 'target_customer_id', 'target_project_id', 'target_environment_id'])]);
|
|
389
|
-
if (d.kind !== r.kind || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id)
|
|
390
|
-
fault();
|
|
391
|
-
const common = { kind: 'streams.register', ownerId: r.owner_id, requesterId: r.requester_id, resultRecipient: recipient(d.result_recipient) };
|
|
392
|
-
let display;
|
|
393
|
-
if (purged) {
|
|
394
|
-
if (d.material_purged !== true)
|
|
395
|
-
fault();
|
|
396
|
-
display = { ...common, materialPurged: true };
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
if (d.binding_source !== r.binding_source || d.connection_id !== r.connection_id || d.linked_wallet_id !== r.linked_wallet_id || d.binding_revision !== r.binding_revision || !assets(d.assets) || !network(d.network) || !text(d.address, 256) || !safeRevision(d.streams_mapping_version) || ![d.streams_mapping_id, d.target_customer_id, d.target_project_id, d.target_environment_id].every(opaqueId))
|
|
400
|
-
fault();
|
|
401
|
-
display = { ...common, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, assets: Object.freeze([...d.assets]), network: d.network, address: d.address, streamsMappingId: d.streams_mapping_id, streamsMappingVersion: d.streams_mapping_version, targetCustomerId: d.target_customer_id, targetProjectId: d.target_project_id, targetEnvironmentId: d.target_environment_id };
|
|
402
|
-
}
|
|
403
|
-
const operation = { id: r.id, version: r.version, kind: 'streams.register', state: r.state, effectState: r.effect_state, authorizationBasis: r.authorization_basis, ownerType: 'account', ownerId: r.owner_id, requesterId: r.requester_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, requestFingerprint: r.request_fingerprint, operationFingerprint: r.operation_fingerprint, display, createdAt: r.created_at, expiresAt: r.expires_at, nextAction: r.next_action };
|
|
404
|
-
if (r.confirmation_location !== undefined)
|
|
405
|
-
operation.confirmationLocation = r.confirmation_location;
|
|
406
|
-
return operation;
|
|
407
|
-
}
|
|
408
|
-
export function parseLinkedRegistrationResult(payload, operationId) {
|
|
409
|
-
const r = record(payload, ['request_id', 'result']);
|
|
410
|
-
const v = record(r.result, ['kind', 'operation_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'registration_digest', 'receipt_id']);
|
|
411
|
-
if (!opaqueId(r.request_id) || !opaqueId(v.operation_id) || v.operation_id !== operationId || v.kind !== 'streams_registration' || v.binding_source !== 'linked_wallet' || !uuid(v.connection_id) || !uuid(v.linked_wallet_id) || !safeRevision(v.binding_revision) || !text(v.source_registration_id, 128) || !text(v.receipt_id, 128) || !digest(v.registration_digest))
|
|
412
|
-
fault();
|
|
413
|
-
return { requestId: r.request_id, result: { kind: 'streams_registration', operationId: v.operation_id, bindingSource: 'linked_wallet', connectionId: v.connection_id, linkedWalletId: v.linked_wallet_id, bindingRevision: v.binding_revision, sourceRegistrationId: v.source_registration_id, registrationDigest: v.registration_digest, receiptId: v.receipt_id } };
|
|
414
|
-
}
|
|
415
|
-
export function linkedRegistrationsPath(linkedWalletId, options = {}) {
|
|
416
|
-
requestUUID(linkedWalletId);
|
|
417
|
-
const declared = record(options, [], ['afterRegistrationId', 'limit', 'signal']);
|
|
418
|
-
const query = new URLSearchParams();
|
|
419
|
-
if (declared.afterRegistrationId !== undefined) {
|
|
420
|
-
requestUUID(declared.afterRegistrationId);
|
|
421
|
-
query.set('after_registration_id', declared.afterRegistrationId);
|
|
422
|
-
}
|
|
423
|
-
readLimit(query, declared.limit);
|
|
424
|
-
return linkedReadPath(linkedWalletId, 'registrations', query);
|
|
425
|
-
}
|
|
426
|
-
export function linkedEventsPath(linkedWalletId, options = {}) {
|
|
427
|
-
requestUUID(linkedWalletId);
|
|
428
|
-
const declared = record(options, [], ['registrationId', 'cursor', 'limit', 'eventTypes', 'signal']);
|
|
429
|
-
const query = new URLSearchParams();
|
|
430
|
-
if (declared.registrationId !== undefined) {
|
|
431
|
-
requestUUID(declared.registrationId);
|
|
432
|
-
query.set('registration_id', declared.registrationId);
|
|
433
|
-
}
|
|
434
|
-
if (declared.cursor !== undefined) {
|
|
435
|
-
if (typeof declared.cursor !== 'string' || declared.cursor.length < 1 || declared.cursor.length > MaxStreamsCursorLength)
|
|
436
|
-
throw new ClientFlowError('invalid_streams_request');
|
|
437
|
-
query.set('cursor', declared.cursor);
|
|
438
|
-
}
|
|
439
|
-
readLimit(query, declared.limit);
|
|
440
|
-
appendEventTypes(query, declared.eventTypes);
|
|
441
|
-
return linkedReadPath(linkedWalletId, 'events', query);
|
|
442
|
-
}
|
|
443
|
-
export function parseLinkedStreamsRegistrationList(payload, linkedWalletId) {
|
|
444
|
-
const root = record(payload, ['items', 'has_more'], ['next_after_registration_id']);
|
|
445
|
-
if (!Array.isArray(root.items) || root.items.length > MaxStreamsPageLimit || typeof root.has_more !== 'boolean' || root.has_more !== Object.hasOwn(root, 'next_after_registration_id'))
|
|
446
|
-
fault();
|
|
447
|
-
const items = root.items.map(item => parseLinkedStreamsRegistration(item, linkedWalletId));
|
|
448
|
-
if (items.some((item, index) => index > 0 && items[index - 1].registrationId >= item.registrationId))
|
|
449
|
-
fault();
|
|
450
|
-
if (root.has_more && (!uuid(root.next_after_registration_id) || items.length === 0 || root.next_after_registration_id !== items.at(-1).registrationId))
|
|
451
|
-
fault();
|
|
452
|
-
const page = { items: Object.freeze(items), hasMore: root.has_more };
|
|
453
|
-
return Object.freeze(root.has_more ? { ...page, nextAfterRegistrationId: root.next_after_registration_id } : page);
|
|
454
|
-
}
|
|
455
|
-
function parseLinkedStreamsRegistration(payload, linkedWalletId) {
|
|
456
|
-
const r = record(payload, ['registration_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'network', 'address', 'assets', 'status', 'effective_from', 'revision', 'source_version']);
|
|
457
|
-
if (!uuid(r.registration_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || r.linked_wallet_id !== linkedWalletId || !safeRevision(r.binding_revision) || !opaqueId(r.source_registration_id) || !network(r.network) || typeof r.address !== 'string' || !/^[A-Za-z0-9_:-]{1,256}$/.test(r.address) || !assets(r.assets) || !['pending', 'active'].includes(r.status) || typeof r.effective_from !== 'string' || new TextEncoder().encode(r.effective_from).length < 1 || new TextEncoder().encode(r.effective_from).length > 256 || !integer(r.revision) || r.revision < 1 || typeof r.source_version !== 'string' || !/^[0-9]+\.[0-9]+$/.test(r.source_version))
|
|
458
|
-
fault();
|
|
459
|
-
return Object.freeze({ registrationId: r.registration_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, sourceRegistrationId: r.source_registration_id, network: r.network, address: r.address, assets: Object.freeze([...r.assets]), status: r.status, effectiveFrom: r.effective_from, revision: r.revision, sourceVersion: r.source_version });
|
|
460
|
-
}
|
|
461
|
-
function linkedReadPath(id, kind, query) {
|
|
462
|
-
const path = `/v1/linked-wallets/${id}/${kind}`;
|
|
463
|
-
return query.size === 0 ? path : `${path}?${query.toString()}`;
|
|
464
|
-
}
|
|
465
|
-
function readLimit(query, value) {
|
|
466
|
-
if (value === undefined)
|
|
467
|
-
return;
|
|
468
|
-
if (!integer(value) || value < 1 || value > MaxStreamsPageLimit)
|
|
469
|
-
throw new ClientFlowError('invalid_streams_request');
|
|
470
|
-
query.set('limit', String(value));
|
|
471
|
-
}
|
package/dist/me.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One wallet of the account: "wallet" is a wallet the provider created,
|
|
3
|
+
* "linked_wallet" one its owner connected, and walletId is an identifier of
|
|
4
|
+
* that kind. A listed linked wallet is active; an own wallet is active or
|
|
5
|
+
* disabled.
|
|
6
|
+
*/
|
|
7
|
+
export interface MeWallet {
|
|
8
|
+
walletId: string;
|
|
9
|
+
kind: "wallet" | "linked_wallet";
|
|
10
|
+
network: string;
|
|
11
|
+
address: string;
|
|
12
|
+
state: "active" | "disabled";
|
|
13
|
+
}
|
|
14
|
+
export interface MeProjection {
|
|
15
|
+
requestId: string;
|
|
16
|
+
account: {
|
|
17
|
+
accountId: string;
|
|
18
|
+
version: string;
|
|
19
|
+
setupRequired: boolean;
|
|
20
|
+
};
|
|
21
|
+
sessionId: string;
|
|
22
|
+
serverTime: number;
|
|
23
|
+
/** Every wallet of the account, own then linked, at most 1000 of each kind. */
|
|
24
|
+
wallets: MeWallet[];
|
|
25
|
+
}
|
|
26
|
+
/** parseMeProjection reads exactly one `/v1/me` answer. */
|
|
27
|
+
export declare function parseMeProjection(payload: unknown): MeProjection;
|
package/dist/me.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live `/v1/me` projection: the current account, the session, the server
|
|
3
|
+
* time and every wallet of the account. It is read as strictly as every
|
|
4
|
+
* other answer: an unknown member, a missing one or a value outside its
|
|
5
|
+
* closed form refuses the whole answer.
|
|
6
|
+
*/
|
|
7
|
+
import { ClientFlowError, canonicalDecimal, integer, opaqueId, record } from "./auth.js";
|
|
8
|
+
/** The most wallets of one kind an account lists; the answer holds two kinds. */
|
|
9
|
+
const maxWalletsPerKind = 1000;
|
|
10
|
+
/** parseMeProjection reads exactly one `/v1/me` answer. */
|
|
11
|
+
export function parseMeProjection(payload) {
|
|
12
|
+
const root = record(payload, ["request_id", "account", "session_id", "server_time", "wallets"]);
|
|
13
|
+
const account = record(root.account, ["account_id", "version", "setup_required"]);
|
|
14
|
+
if (!opaqueId(root.request_id) || !opaqueId(account.account_id) || !canonicalDecimal(account.version) || typeof account.setup_required !== "boolean" || !opaqueId(root.session_id) || !integer(root.server_time) ||
|
|
15
|
+
!Array.isArray(root.wallets) || root.wallets.length > 2 * maxWalletsPerKind) {
|
|
16
|
+
throw new ClientFlowError("invalid_server_response");
|
|
17
|
+
}
|
|
18
|
+
const wallets = root.wallets.map((value) => {
|
|
19
|
+
const item = record(value, ["wallet_id", "kind", "network", "address", "state"]);
|
|
20
|
+
if (!opaqueId(item.wallet_id) || (item.kind !== "wallet" && item.kind !== "linked_wallet") || typeof item.network !== "string" || item.network === "" ||
|
|
21
|
+
typeof item.address !== "string" || item.address === "" || item.address.length > 256 || (item.state !== "active" && item.state !== "disabled")) {
|
|
22
|
+
throw new ClientFlowError("invalid_server_response");
|
|
23
|
+
}
|
|
24
|
+
return { walletId: item.wallet_id, kind: item.kind, network: item.network, address: item.address, state: item.state };
|
|
25
|
+
});
|
|
26
|
+
if (wallets.filter((wallet) => wallet.kind === "wallet").length > maxWalletsPerKind || wallets.filter((wallet) => wallet.kind === "linked_wallet").length > maxWalletsPerKind) {
|
|
27
|
+
throw new ClientFlowError("invalid_server_response");
|
|
28
|
+
}
|
|
29
|
+
return { requestId: root.request_id, account: { accountId: account.account_id, version: account.version, setupRequired: account.setup_required }, sessionId: root.session_id, serverTime: root.server_time, wallets };
|
|
30
|
+
}
|
package/dist/mobile.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface MobileSecureStorage {
|
|
2
|
+
get(key: string): Promise<string | null>;
|
|
3
|
+
set(key: string, value: string): Promise<void>;
|
|
4
|
+
delete(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface MobileAssociationInput {
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
applicationIdentifier: string;
|
|
9
|
+
}
|
|
10
|
+
export interface MobileAssociationVerification {
|
|
11
|
+
source: "os_verified_https_association";
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
applicationIdentifier: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Host/native trust boundary. Implementations must ask the operating system to
|
|
17
|
+
* verify the configured HTTPS app/universal-link association. On iOS, where no
|
|
18
|
+
* API reports that verification, the system makes it when an authentication
|
|
19
|
+
* session with an HTTPS callback starts, and refuses to start one for an app
|
|
20
|
+
* the domain does not name; a bridge whose openSystemBrowser opens exactly
|
|
21
|
+
* that session answers for it. This result is a port contract, not
|
|
22
|
+
* cryptographic proof produced by this TypeScript package.
|
|
23
|
+
*/
|
|
24
|
+
export interface MobileBrowserBridge {
|
|
25
|
+
verifyAssociatedRedirect(input: MobileAssociationInput): Promise<MobileAssociationVerification | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Opens the URL in the system browser's authentication session
|
|
28
|
+
* (ASWebAuthenticationSession on iOS, a Custom Tab on Android), never an
|
|
29
|
+
* embedded web view, and resolves with the URL that session ended on: the
|
|
30
|
+
* registered app link the provider redirected back to. It resolves with
|
|
31
|
+
* nothing when no app link came back, for example when the person closed
|
|
32
|
+
* the browser. A sign-in needs that URL; the other hosted flows take it
|
|
33
|
+
* from the host through their `complete…` method.
|
|
34
|
+
*/
|
|
35
|
+
openSystemBrowser(url: string): Promise<string | void> | string | void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* verifyMobileAssociation asks the host to have the operating system verify
|
|
39
|
+
* the app link of the exact configured redirect for the configured app, and
|
|
40
|
+
* accepts only the exact answer for both. It runs when the client is
|
|
41
|
+
* initialized and again before every sign-in opens the system browser.
|
|
42
|
+
*/
|
|
43
|
+
export declare function verifyMobileAssociation(bridge: MobileBrowserBridge, redirectUri: string, applicationIdentifier: string): Promise<void>;
|
|
44
|
+
/** mobileBridgeComplete checks that the host supplied every mobile port. */
|
|
45
|
+
export declare function mobileBridgeComplete(bridge: MobileBrowserBridge | undefined, storage: MobileSecureStorage | undefined): bridge is MobileBrowserBridge;
|
|
46
|
+
/** mobileApplicationIdentifier is a bundle or package identifier as configuration names it. */
|
|
47
|
+
export declare function mobileApplicationIdentifier(value: unknown): value is string;
|
|
48
|
+
/**
|
|
49
|
+
* mobileSignInReturn reads what the system browser handed back from a hosted
|
|
50
|
+
* sign-in: the exact registered redirect with the one-use code and this
|
|
51
|
+
* sign-in's own state, and nothing else. No app link back, a redirect of
|
|
52
|
+
* another sign-in or an attempt's redirect is refused before any exchange.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mobileSignInReturn(returned: string | void, redirectUri: string, state: string): {
|
|
55
|
+
code: string;
|
|
56
|
+
state: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* mobileRefreshKey names the refresh token of one public client in the secure
|
|
60
|
+
* storage. Its parts are opaque identifiers, which hold no dot, so the key is
|
|
61
|
+
* unambiguous, and it uses only letters, digits, dots, hyphens and
|
|
62
|
+
* underscores, which every platform keystore takes as a key.
|
|
63
|
+
*/
|
|
64
|
+
export declare function mobileRefreshKey(tenantId: string, applicationId: string, environmentId: string, clientId: string): string;
|
package/dist/mobile.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The mobile host boundary: the ports a native host supplies (the system
|
|
2
|
+
// browser with its verified app link, and the OS secure storage that holds the
|
|
3
|
+
// refresh token), the check of the app link the host reports, and the return
|
|
4
|
+
// of a sign-in from the system browser's authentication session.
|
|
5
|
+
//
|
|
6
|
+
// A mobile app sends no browser Origin. The provider binds its sign-in to the
|
|
7
|
+
// exact registered redirect, an HTTPS app link (iOS universal link, Android
|
|
8
|
+
// app link) on one of the client's registered origins, and to the state and
|
|
9
|
+
// the S256 verifier this client keeps in memory. The browser comes back to the
|
|
10
|
+
// app with the one-use code and the state alone; the access token stays in
|
|
11
|
+
// memory and the refresh token goes only to `MobileSecureStorage`.
|
|
12
|
+
import { ClientFlowError } from "./auth.js";
|
|
13
|
+
import { handoffCallback } from "./redirects.js";
|
|
14
|
+
/**
|
|
15
|
+
* verifyMobileAssociation asks the host to have the operating system verify
|
|
16
|
+
* the app link of the exact configured redirect for the configured app, and
|
|
17
|
+
* accepts only the exact answer for both. It runs when the client is
|
|
18
|
+
* initialized and again before every sign-in opens the system browser.
|
|
19
|
+
*/
|
|
20
|
+
export async function verifyMobileAssociation(bridge, redirectUri, applicationIdentifier) {
|
|
21
|
+
let verification;
|
|
22
|
+
try {
|
|
23
|
+
verification = await bridge.verifyAssociatedRedirect({ redirectUri, applicationIdentifier });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new ClientFlowError("mobile_association_unverified");
|
|
27
|
+
}
|
|
28
|
+
if (!exactMobileAssociation(verification, redirectUri, applicationIdentifier))
|
|
29
|
+
throw new ClientFlowError("mobile_association_unverified");
|
|
30
|
+
}
|
|
31
|
+
/** mobileBridgeComplete checks that the host supplied every mobile port. */
|
|
32
|
+
export function mobileBridgeComplete(bridge, storage) {
|
|
33
|
+
return Boolean(storage) && Boolean(bridge) && typeof bridge?.verifyAssociatedRedirect === "function" && typeof bridge?.openSystemBrowser === "function";
|
|
34
|
+
}
|
|
35
|
+
/** mobileApplicationIdentifier is a bundle or package identifier as configuration names it. */
|
|
36
|
+
export function mobileApplicationIdentifier(value) {
|
|
37
|
+
return typeof value === "string" && /^[A-Za-z0-9._-]{1,255}$/.test(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* mobileSignInReturn reads what the system browser handed back from a hosted
|
|
41
|
+
* sign-in: the exact registered redirect with the one-use code and this
|
|
42
|
+
* sign-in's own state, and nothing else. No app link back, a redirect of
|
|
43
|
+
* another sign-in or an attempt's redirect is refused before any exchange.
|
|
44
|
+
*/
|
|
45
|
+
export function mobileSignInReturn(returned, redirectUri, state) {
|
|
46
|
+
if (typeof returned !== "string")
|
|
47
|
+
throw new ClientFlowError("mobile_redirect_unavailable");
|
|
48
|
+
const callback = handoffCallback(returned, redirectUri);
|
|
49
|
+
if (callback.attemptId !== undefined)
|
|
50
|
+
throw new ClientFlowError("invalid_callback");
|
|
51
|
+
if (callback.state !== state)
|
|
52
|
+
throw new ClientFlowError("state_mismatch");
|
|
53
|
+
return { code: callback.code, state: callback.state };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* mobileRefreshKey names the refresh token of one public client in the secure
|
|
57
|
+
* storage. Its parts are opaque identifiers, which hold no dot, so the key is
|
|
58
|
+
* unambiguous, and it uses only letters, digits, dots, hyphens and
|
|
59
|
+
* underscores, which every platform keystore takes as a key.
|
|
60
|
+
*/
|
|
61
|
+
export function mobileRefreshKey(tenantId, applicationId, environmentId, clientId) {
|
|
62
|
+
return ["authorization", tenantId, applicationId, environmentId, clientId, "refresh"].join(".");
|
|
63
|
+
}
|
|
64
|
+
function exactMobileAssociation(value, redirectUri, applicationIdentifier) {
|
|
65
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
66
|
+
return false;
|
|
67
|
+
const result = value;
|
|
68
|
+
return Object.keys(result).length === 3 && result.source === "os_verified_https_association" && result.redirectUri === redirectUri && result.applicationIdentifier === applicationIdentifier;
|
|
69
|
+
}
|