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