@interop/wallet-request 0.1.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/LICENSE.md +20 -0
- package/README.md +160 -0
- package/dist/appKey.d.ts +295 -0
- package/dist/appKey.d.ts.map +1 -0
- package/dist/appKey.js +516 -0
- package/dist/appKey.js.map +1 -0
- package/dist/capabilityRequest.d.ts +41 -0
- package/dist/capabilityRequest.d.ts.map +1 -0
- package/dist/capabilityRequest.js +46 -0
- package/dist/capabilityRequest.js.map +1 -0
- package/dist/classify.d.ts +233 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +419 -0
- package/dist/classify.js.map +1 -0
- package/dist/composeVp.d.ts +52 -0
- package/dist/composeVp.d.ts.map +1 -0
- package/dist/composeVp.js +176 -0
- package/dist/composeVp.js.map +1 -0
- package/dist/ephemeralExchange.d.ts +100 -0
- package/dist/ephemeralExchange.d.ts.map +1 -0
- package/dist/ephemeralExchange.js +230 -0
- package/dist/ephemeralExchange.js.map +1 -0
- package/dist/exchangeClient.d.ts +142 -0
- package/dist/exchangeClient.d.ts.map +1 -0
- package/dist/exchangeClient.js +241 -0
- package/dist/exchangeClient.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/interactionRequest.d.ts +23 -0
- package/dist/interactionRequest.d.ts.map +1 -0
- package/dist/interactionRequest.js +46 -0
- package/dist/interactionRequest.js.map +1 -0
- package/dist/interactionUrl.d.ts +42 -0
- package/dist/interactionUrl.d.ts.map +1 -0
- package/dist/interactionUrl.js +104 -0
- package/dist/interactionUrl.js.map +1 -0
- package/dist/log.d.ts +49 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +53 -0
- package/dist/log.js.map +1 -0
- package/dist/matching.d.ts +95 -0
- package/dist/matching.d.ts.map +1 -0
- package/dist/matching.js +205 -0
- package/dist/matching.js.map +1 -0
- package/dist/onboarding.d.ts +96 -0
- package/dist/onboarding.d.ts.map +1 -0
- package/dist/onboarding.js +174 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/parse.d.ts +63 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +153 -0
- package/dist/parse.js.map +1 -0
- package/dist/presentationSuite.d.ts +71 -0
- package/dist/presentationSuite.d.ts.map +1 -0
- package/dist/presentationSuite.js +143 -0
- package/dist/presentationSuite.js.map +1 -0
- package/dist/processRequest.d.ts +45 -0
- package/dist/processRequest.d.ts.map +1 -0
- package/dist/processRequest.js +146 -0
- package/dist/processRequest.js.map +1 -0
- package/dist/queryPredicates.d.ts +112 -0
- package/dist/queryPredicates.d.ts.map +1 -0
- package/dist/queryPredicates.js +157 -0
- package/dist/queryPredicates.js.map +1 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/walletInput.d.ts +118 -0
- package/dist/walletInput.d.ts.map +1 -0
- package/dist/walletInput.js +169 -0
- package/dist/walletInput.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { IVerifiableCredential, IVPRDetails, PresentationSigner, RequestProcessors, WalletResponse } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Domain-binding check (VCALM §3.4.3 advisement): a DID-Auth `domain` MUST match
|
|
4
|
+
* the channel the request arrived on, otherwise a dishonest verifier could relay
|
|
5
|
+
* the challenge from another origin and replay the response.
|
|
6
|
+
*
|
|
7
|
+
* @param options {object}
|
|
8
|
+
* @param options.domain {string} - The `domain` from the request.
|
|
9
|
+
* @param [options.origin] {string} - The channel origin (for CHAPI,
|
|
10
|
+
* `event.credentialRequestOrigin`).
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export declare function domainMatchesOrigin({ domain, origin }: {
|
|
14
|
+
domain: string;
|
|
15
|
+
origin?: string;
|
|
16
|
+
}): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Processes a Verifiable Presentation Request and composes the wallet's
|
|
19
|
+
* response. Assumes the user has already consented and (for VC sharing) picked
|
|
20
|
+
* which credentials to send.
|
|
21
|
+
*
|
|
22
|
+
* @param options {object}
|
|
23
|
+
* @param options.request {IVPRDetails} - The VPR body.
|
|
24
|
+
* @param options.presentationSigner {PresentationSigner} - Authentication signer
|
|
25
|
+
* and holder DID.
|
|
26
|
+
* @param [options.selectedVCs] {IVerifiableCredential[]} - VCs the user chose to
|
|
27
|
+
* share (empty for a DID-Auth-only or zcap-only response).
|
|
28
|
+
* @param [options.credentialRequestOrigin] {string} - Channel origin, used for
|
|
29
|
+
* the domain-binding check and required by the App Connect branch.
|
|
30
|
+
* @param [options.processors] {RequestProcessors} - App-side capability /
|
|
31
|
+
* App Connect processors.
|
|
32
|
+
* @param [options.cryptosuite] {string} - Cryptosuite override; when absent it
|
|
33
|
+
* is negotiated from the request's `acceptedCryptosuites`.
|
|
34
|
+
* @returns {Promise<WalletResponse>} The response VP (and any granted zcaps), or
|
|
35
|
+
* `{}` when there is nothing to send.
|
|
36
|
+
*/
|
|
37
|
+
export declare function processRequest({ request, presentationSigner, selectedVCs, credentialRequestOrigin, processors, cryptosuite }: {
|
|
38
|
+
request: IVPRDetails;
|
|
39
|
+
presentationSigner: PresentationSigner;
|
|
40
|
+
selectedVCs?: IVerifiableCredential[];
|
|
41
|
+
credentialRequestOrigin?: string;
|
|
42
|
+
processors?: RequestProcessors;
|
|
43
|
+
cryptosuite?: string;
|
|
44
|
+
}): Promise<WalletResponse>;
|
|
45
|
+
//# sourceMappingURL=processRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processRequest.d.ts","sourceRoot":"","sources":["../src/processRequest.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,qBAAqB,EACrB,WAAW,EAEX,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACf,MAAM,YAAY,CAAA;AAkBnB;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,MAAM,EACP,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAOV;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,cAAc,CAAC,EACnC,OAAO,EACP,kBAAkB,EAClB,WAAgB,EAChB,uBAAuB,EACvB,UAAU,EACV,WAAW,EACZ,EAAE;IACD,OAAO,EAAE,WAAW,CAAA;IACpB,kBAAkB,EAAE,kBAAkB,CAAA;IACtC,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACrC,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CA+E1B"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Framework-agnostic request processing: turns a classified VPR body plus the
|
|
6
|
+
* user's VC selection into a {@link WalletResponse} (a possibly-signed VP, plus
|
|
7
|
+
* any delegated zcaps). The response channel (CHAPI `respondWith`, an
|
|
8
|
+
* exchange-URL POST) stays with the caller -- this function only returns data,
|
|
9
|
+
* and assumes the user has already consented and picked which credentials to
|
|
10
|
+
* send.
|
|
11
|
+
*
|
|
12
|
+
* Ported from Freewallet's `src/lib/walletRequest/processRequest.ts` (the pure
|
|
13
|
+
* shape). The two app-specific side effects -- capability delegation and the App
|
|
14
|
+
* Connect branch -- are injected as {@link RequestProcessors} rather than
|
|
15
|
+
* imported, so this layer carries no session / grant-resolution machinery. The
|
|
16
|
+
* signer and holder DID are injected as a {@link PresentationSigner}.
|
|
17
|
+
*/
|
|
18
|
+
import { log } from './log.js';
|
|
19
|
+
import { appConnectRequestOf, classifyRequest, queriesOf } from './classify.js';
|
|
20
|
+
import { composeVp } from './composeVp.js';
|
|
21
|
+
import { negotiateCryptosuite } from './presentationSuite.js';
|
|
22
|
+
import { hasAppConnectQuery } from './queryPredicates.js';
|
|
23
|
+
/**
|
|
24
|
+
* Extracts the host (`host:port`) from a value that may be a full URL or a bare
|
|
25
|
+
* host / host:port. Returns undefined if it cannot be parsed.
|
|
26
|
+
*/
|
|
27
|
+
function hostOf(value) {
|
|
28
|
+
try {
|
|
29
|
+
const url = value.includes('://')
|
|
30
|
+
? new URL(value)
|
|
31
|
+
: new URL(`https://${value}`);
|
|
32
|
+
return url.host;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
log.warn('Could not parse host', { value, err });
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Domain-binding check (VCALM §3.4.3 advisement): a DID-Auth `domain` MUST match
|
|
41
|
+
* the channel the request arrived on, otherwise a dishonest verifier could relay
|
|
42
|
+
* the challenge from another origin and replay the response.
|
|
43
|
+
*
|
|
44
|
+
* @param options {object}
|
|
45
|
+
* @param options.domain {string} - The `domain` from the request.
|
|
46
|
+
* @param [options.origin] {string} - The channel origin (for CHAPI,
|
|
47
|
+
* `event.credentialRequestOrigin`).
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function domainMatchesOrigin({ domain, origin }) {
|
|
51
|
+
if (!origin) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const originHost = hostOf(origin);
|
|
55
|
+
const domainHost = hostOf(domain);
|
|
56
|
+
return !!originHost && originHost === domainHost;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Processes a Verifiable Presentation Request and composes the wallet's
|
|
60
|
+
* response. Assumes the user has already consented and (for VC sharing) picked
|
|
61
|
+
* which credentials to send.
|
|
62
|
+
*
|
|
63
|
+
* @param options {object}
|
|
64
|
+
* @param options.request {IVPRDetails} - The VPR body.
|
|
65
|
+
* @param options.presentationSigner {PresentationSigner} - Authentication signer
|
|
66
|
+
* and holder DID.
|
|
67
|
+
* @param [options.selectedVCs] {IVerifiableCredential[]} - VCs the user chose to
|
|
68
|
+
* share (empty for a DID-Auth-only or zcap-only response).
|
|
69
|
+
* @param [options.credentialRequestOrigin] {string} - Channel origin, used for
|
|
70
|
+
* the domain-binding check and required by the App Connect branch.
|
|
71
|
+
* @param [options.processors] {RequestProcessors} - App-side capability /
|
|
72
|
+
* App Connect processors.
|
|
73
|
+
* @param [options.cryptosuite] {string} - Cryptosuite override; when absent it
|
|
74
|
+
* is negotiated from the request's `acceptedCryptosuites`.
|
|
75
|
+
* @returns {Promise<WalletResponse>} The response VP (and any granted zcaps), or
|
|
76
|
+
* `{}` when there is nothing to send.
|
|
77
|
+
*/
|
|
78
|
+
export async function processRequest({ request, presentationSigner, selectedVCs = [], credentialRequestOrigin, processors, cryptosuite }) {
|
|
79
|
+
const { didAuth, zcapRequests } = classifyRequest(request);
|
|
80
|
+
const queries = queriesOf(request);
|
|
81
|
+
const { challenge, domain } = request;
|
|
82
|
+
// Honor any cryptosuite the verifier asks for (VCALM `acceptedCryptosuites`),
|
|
83
|
+
// unless the caller pinned one.
|
|
84
|
+
const negotiatedCryptosuite = cryptosuite ?? negotiateCryptosuite(queries);
|
|
85
|
+
// Security: never sign an authentication proof bound to a domain the request
|
|
86
|
+
// did not actually arrive from. Enforced whenever a `domain` is present,
|
|
87
|
+
// including a zcap-only request whose (unsigned) VP still names an origin.
|
|
88
|
+
if (domain &&
|
|
89
|
+
!domainMatchesOrigin({ domain, origin: credentialRequestOrigin })) {
|
|
90
|
+
throw new Error(`DID Auth domain "${domain}" does not match request origin ` +
|
|
91
|
+
`"${credentialRequestOrigin}".`);
|
|
92
|
+
}
|
|
93
|
+
// An App Connect request takes its own single-round branch, handled by the
|
|
94
|
+
// injected processor. The requesting origin is what the app key is bound to
|
|
95
|
+
// (and what the query's `appUrl` is validated against), so it is required;
|
|
96
|
+
// the query itself is validated here (`appConnectRequestOf` throws on a
|
|
97
|
+
// malformed `app` block), so the processor only ever sees a well-formed
|
|
98
|
+
// request whose `appUrl` is already in serialized form.
|
|
99
|
+
if (hasAppConnectQuery(queries)) {
|
|
100
|
+
if (!processors?.processAppConnect) {
|
|
101
|
+
throw new Error('An App Connect request was received but no processAppConnect ' +
|
|
102
|
+
'processor was provided.');
|
|
103
|
+
}
|
|
104
|
+
if (!credentialRequestOrigin) {
|
|
105
|
+
throw new Error('An App Connect request requires a requesting origin.');
|
|
106
|
+
}
|
|
107
|
+
const appConnect = appConnectRequestOf({
|
|
108
|
+
queries,
|
|
109
|
+
origin: credentialRequestOrigin
|
|
110
|
+
});
|
|
111
|
+
if (!appConnect) {
|
|
112
|
+
throw new Error('An AppConnectQuery could not be classified.');
|
|
113
|
+
}
|
|
114
|
+
return processors.processAppConnect({
|
|
115
|
+
request,
|
|
116
|
+
appConnect,
|
|
117
|
+
origin: credentialRequestOrigin,
|
|
118
|
+
challenge,
|
|
119
|
+
domain,
|
|
120
|
+
didAuthRequested: didAuth,
|
|
121
|
+
cryptosuite: negotiatedCryptosuite
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Delegate the approved capabilities first, then embed them in the VP.
|
|
125
|
+
const zcaps = zcapRequests.length > 0 && processors?.processZcaps
|
|
126
|
+
? await processors.processZcaps({ zcapRequests })
|
|
127
|
+
: [];
|
|
128
|
+
if (!didAuth && selectedVCs.length === 0 && zcaps.length === 0) {
|
|
129
|
+
// Nothing to send: no DID Auth, no VCs, and no satisfiable grants.
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
const verifiablePresentation = await composeVp({
|
|
133
|
+
presentationSigner,
|
|
134
|
+
selectedVcs: selectedVCs,
|
|
135
|
+
challenge,
|
|
136
|
+
domain,
|
|
137
|
+
didAuthRequested: didAuth,
|
|
138
|
+
cryptosuite: negotiatedCryptosuite,
|
|
139
|
+
zcaps
|
|
140
|
+
});
|
|
141
|
+
// Return the delegated capabilities alongside the VP so the caller can log
|
|
142
|
+
// exactly what was granted from these objects, rather than reading them back
|
|
143
|
+
// off the composed VP.
|
|
144
|
+
return { verifiablePresentation, zcaps };
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=processRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processRequest.js","sourceRoot":"","sources":["../src/processRequest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Import-free helpers shared by the request classifiers: which VPR query
|
|
6
|
+
* type a query object carries, and the parse-and-no-fragment core every URL
|
|
7
|
+
* validated straight off a request body layers its own rule on top of. Kept
|
|
8
|
+
* in one dependency-light leaf file so `onboarding.ts` can classify a
|
|
9
|
+
* `WalletOnboardingQuery` without pulling `classify.ts`'s heavier graph.
|
|
10
|
+
*/
|
|
11
|
+
import type { IVPRQuery, IZcapQuery } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Whether a query is a standalone capability query, under either type string:
|
|
14
|
+
* `AuthorizationCapabilityQuery` (the canonical VCALM spelling) or the legacy
|
|
15
|
+
* `ZcapQuery`.
|
|
16
|
+
*
|
|
17
|
+
* The one reader of that alias pair. Both exclusivity checks and both
|
|
18
|
+
* capability extractors ask through here, so retiring or extending the pair
|
|
19
|
+
* is one edit rather than four.
|
|
20
|
+
*
|
|
21
|
+
* @param query {IVPRQuery}
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export declare function isZcapQuery(query: IVPRQuery): query is IZcapQuery;
|
|
25
|
+
/**
|
|
26
|
+
* The query types that stand alone in a request under the
|
|
27
|
+
* one-mental-model-per-exchange rule: a screen that asks the person to
|
|
28
|
+
* connect a wallet, or to join an account, must not simultaneously ask them
|
|
29
|
+
* to share credentials or grant capabilities. Each type excludes
|
|
30
|
+
* `QueryByExample`, standalone capability queries, and every other member of
|
|
31
|
+
* this set, in both directions. The set is the one home of that mutual
|
|
32
|
+
* exclusion: adding a third exclusive type is one entry here, and
|
|
33
|
+
* {@link exclusiveQueryOf} refuses the new mixtures from every side.
|
|
34
|
+
*/
|
|
35
|
+
export declare const EXCLUSIVE_QUERY_TYPES: readonly ["AppConnectQuery", "WalletOnboardingQuery"];
|
|
36
|
+
/**
|
|
37
|
+
* A member of {@link EXCLUSIVE_QUERY_TYPES}.
|
|
38
|
+
*/
|
|
39
|
+
export type ExclusiveQueryType = (typeof EXCLUSIVE_QUERY_TYPES)[number];
|
|
40
|
+
/**
|
|
41
|
+
* Whether a query is an `AppConnectQuery`. Callers filtering on this still
|
|
42
|
+
* cast the result to `IAppConnectQuery[]` (see {@link isQueryOfType}).
|
|
43
|
+
*
|
|
44
|
+
* @param query {IVPRQuery}
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export declare function isAppConnectQuery(query: IVPRQuery): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a query set carries an `AppConnectQuery` at all: the gate
|
|
50
|
+
* `processRequest` takes its App Connect branch on, and the presence half of
|
|
51
|
+
* `appConnectRequestOf`'s extraction. The gate cannot simply run the
|
|
52
|
+
* extractor, since the extractor needs the requesting origin whose absence
|
|
53
|
+
* the gate must report, so the two share this predicate instead of each
|
|
54
|
+
* filtering on the type string.
|
|
55
|
+
*
|
|
56
|
+
* @param queries {IVPRQuery[]}
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export declare function hasAppConnectQuery(queries: IVPRQuery[]): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether a query is a `WalletOnboardingQuery`. Matched the same way as
|
|
62
|
+
* {@link isAppConnectQuery}.
|
|
63
|
+
*
|
|
64
|
+
* @param query {IVPRQuery}
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
export declare function isWalletOnboardingQuery(query: IVPRQuery): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The singleton query of a set for one of the {@link EXCLUSIVE_QUERY_TYPES}.
|
|
70
|
+
* Returns `null` when no query of the type is present, the one query when
|
|
71
|
+
* exactly one is, and throws when more than one appears or when a query of a
|
|
72
|
+
* mutually exclusive type sits beside it: `QueryByExample`, a standalone
|
|
73
|
+
* capability query, or any other exclusive type. The exclusion list is
|
|
74
|
+
* derived from the shared set rather than written per caller, so no type can
|
|
75
|
+
* be excluded from one direction only.
|
|
76
|
+
*
|
|
77
|
+
* @param options {object}
|
|
78
|
+
* @param options.queries {IVPRQuery[]} the request's query set
|
|
79
|
+
* @param options.typeName {ExclusiveQueryType} the singleton's type
|
|
80
|
+
* @returns {IVPRQuery | null}
|
|
81
|
+
*/
|
|
82
|
+
export declare function exclusiveQueryOf({ queries, typeName }: {
|
|
83
|
+
queries: IVPRQuery[];
|
|
84
|
+
typeName: ExclusiveQueryType;
|
|
85
|
+
}): IVPRQuery | null;
|
|
86
|
+
/**
|
|
87
|
+
* Parses a wire value as an absolute URL with no fragment: the shared core
|
|
88
|
+
* every request field that copies a URL verbatim from an untrusted body
|
|
89
|
+
* layers its own rule on top of (an App Connect `appUrl`'s same-origin check,
|
|
90
|
+
* a wallet-onboarding `host`'s http(s)-only check). Throws a caller-supplied
|
|
91
|
+
* message on a parse failure (with the parse error as `cause`) or on a
|
|
92
|
+
* fragment, so each site keeps its own wording while the parse-and-no-
|
|
93
|
+
* fragment logic is written once.
|
|
94
|
+
*
|
|
95
|
+
* The fragment check reads the serialized URL rather than `url.hash`: a bare
|
|
96
|
+
* trailing `#` sets an empty (non-null) fragment that `hash` reports as `''`,
|
|
97
|
+
* and a percent-encoded `%23` never appears as `#` in the serialization.
|
|
98
|
+
*
|
|
99
|
+
* @param options {object}
|
|
100
|
+
* @param options.value {string} the raw wire value to parse
|
|
101
|
+
* @param options.notAbsoluteMessage {string} thrown, with the parse error
|
|
102
|
+
* as `cause`, when `value` does not parse as an absolute URL
|
|
103
|
+
* @param options.fragmentMessage {string} thrown when the parsed URL
|
|
104
|
+
* carries a fragment
|
|
105
|
+
* @returns {URL} the parsed URL
|
|
106
|
+
*/
|
|
107
|
+
export declare function parsedAbsoluteUrl({ value, notAbsoluteMessage, fragmentMessage }: {
|
|
108
|
+
value: string;
|
|
109
|
+
notAbsoluteMessage: string;
|
|
110
|
+
fragmentMessage: string;
|
|
111
|
+
}): URL;
|
|
112
|
+
//# sourceMappingURL=queryPredicates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryPredicates.d.ts","sourceRoot":"","sources":["../src/queryPredicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEvD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,IAAI,UAAU,CAIjE;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,uDAGxB,CAAA;AAEV;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAwBvE;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAE3D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEjE;AAYD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,QAAQ,EACT,EAAE;IACD,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,QAAQ,EAAE,kBAAkB,CAAA;CAC7B,GAAG,SAAS,GAAG,IAAI,CAuBnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,kBAAkB,EAClB,eAAe,EAChB,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,kBAAkB,EAAE,MAAM,CAAA;IAC1B,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,GAAG,CAWN"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a query is a standalone capability query, under either type string:
|
|
3
|
+
* `AuthorizationCapabilityQuery` (the canonical VCALM spelling) or the legacy
|
|
4
|
+
* `ZcapQuery`.
|
|
5
|
+
*
|
|
6
|
+
* The one reader of that alias pair. Both exclusivity checks and both
|
|
7
|
+
* capability extractors ask through here, so retiring or extending the pair
|
|
8
|
+
* is one edit rather than four.
|
|
9
|
+
*
|
|
10
|
+
* @param query {IVPRQuery}
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function isZcapQuery(query) {
|
|
14
|
+
return (query.type === 'AuthorizationCapabilityQuery' || query.type === 'ZcapQuery');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The query types that stand alone in a request under the
|
|
18
|
+
* one-mental-model-per-exchange rule: a screen that asks the person to
|
|
19
|
+
* connect a wallet, or to join an account, must not simultaneously ask them
|
|
20
|
+
* to share credentials or grant capabilities. Each type excludes
|
|
21
|
+
* `QueryByExample`, standalone capability queries, and every other member of
|
|
22
|
+
* this set, in both directions. The set is the one home of that mutual
|
|
23
|
+
* exclusion: adding a third exclusive type is one entry here, and
|
|
24
|
+
* {@link exclusiveQueryOf} refuses the new mixtures from every side.
|
|
25
|
+
*/
|
|
26
|
+
export const EXCLUSIVE_QUERY_TYPES = [
|
|
27
|
+
'AppConnectQuery',
|
|
28
|
+
'WalletOnboardingQuery'
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Whether a query carries the given `type` string. The one comparison every
|
|
32
|
+
* type test in this file runs through: `AppConnectQuery` and
|
|
33
|
+
* `WalletOnboardingQuery` extend the spec query union rather than being part
|
|
34
|
+
* of it, so they are matched by `type` string and upcast rather than narrowed
|
|
35
|
+
* via a type predicate.
|
|
36
|
+
*
|
|
37
|
+
* @param options {object}
|
|
38
|
+
* @param options.query {IVPRQuery}
|
|
39
|
+
* @param options.typeName {string}
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
function isQueryOfType({ query, typeName }) {
|
|
43
|
+
return query.type === typeName;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a query is an `AppConnectQuery`. Callers filtering on this still
|
|
47
|
+
* cast the result to `IAppConnectQuery[]` (see {@link isQueryOfType}).
|
|
48
|
+
*
|
|
49
|
+
* @param query {IVPRQuery}
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function isAppConnectQuery(query) {
|
|
53
|
+
return isQueryOfType({ query, typeName: 'AppConnectQuery' });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether a query set carries an `AppConnectQuery` at all: the gate
|
|
57
|
+
* `processRequest` takes its App Connect branch on, and the presence half of
|
|
58
|
+
* `appConnectRequestOf`'s extraction. The gate cannot simply run the
|
|
59
|
+
* extractor, since the extractor needs the requesting origin whose absence
|
|
60
|
+
* the gate must report, so the two share this predicate instead of each
|
|
61
|
+
* filtering on the type string.
|
|
62
|
+
*
|
|
63
|
+
* @param queries {IVPRQuery[]}
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export function hasAppConnectQuery(queries) {
|
|
67
|
+
return queries.some(isAppConnectQuery);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a query is a `WalletOnboardingQuery`. Matched the same way as
|
|
71
|
+
* {@link isAppConnectQuery}.
|
|
72
|
+
*
|
|
73
|
+
* @param query {IVPRQuery}
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isWalletOnboardingQuery(query) {
|
|
77
|
+
return isQueryOfType({ query, typeName: 'WalletOnboardingQuery' });
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The indefinite article a type name takes in a refusal message.
|
|
81
|
+
*
|
|
82
|
+
* @param typeName {string}
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function articleFor(typeName) {
|
|
86
|
+
return /^[AEIOU]/.test(typeName) ? 'an' : 'a';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The singleton query of a set for one of the {@link EXCLUSIVE_QUERY_TYPES}.
|
|
90
|
+
* Returns `null` when no query of the type is present, the one query when
|
|
91
|
+
* exactly one is, and throws when more than one appears or when a query of a
|
|
92
|
+
* mutually exclusive type sits beside it: `QueryByExample`, a standalone
|
|
93
|
+
* capability query, or any other exclusive type. The exclusion list is
|
|
94
|
+
* derived from the shared set rather than written per caller, so no type can
|
|
95
|
+
* be excluded from one direction only.
|
|
96
|
+
*
|
|
97
|
+
* @param options {object}
|
|
98
|
+
* @param options.queries {IVPRQuery[]} the request's query set
|
|
99
|
+
* @param options.typeName {ExclusiveQueryType} the singleton's type
|
|
100
|
+
* @returns {IVPRQuery | null}
|
|
101
|
+
*/
|
|
102
|
+
export function exclusiveQueryOf({ queries, typeName }) {
|
|
103
|
+
const matches = queries.filter(query => isQueryOfType({ query, typeName }));
|
|
104
|
+
if (matches.length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (matches.length > 1) {
|
|
108
|
+
throw new Error(`More than one ${typeName} found, exiting.`);
|
|
109
|
+
}
|
|
110
|
+
const otherTypes = EXCLUSIVE_QUERY_TYPES.filter(other => other !== typeName);
|
|
111
|
+
const isExcluded = (query) => query.type === 'QueryByExample' ||
|
|
112
|
+
isZcapQuery(query) ||
|
|
113
|
+
otherTypes.includes(query.type);
|
|
114
|
+
if (queries.some(isExcluded)) {
|
|
115
|
+
const others = otherTypes.map(other => `${articleFor(other)} ${other}`);
|
|
116
|
+
const article = articleFor(typeName);
|
|
117
|
+
const capitalized = article[0].toUpperCase() + article.slice(1);
|
|
118
|
+
throw new Error(`${capitalized} ${typeName} cannot be combined with QueryByExample, ` +
|
|
119
|
+
`standalone capability queries, or ${others.join(', or ')}.`);
|
|
120
|
+
}
|
|
121
|
+
return matches[0];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parses a wire value as an absolute URL with no fragment: the shared core
|
|
125
|
+
* every request field that copies a URL verbatim from an untrusted body
|
|
126
|
+
* layers its own rule on top of (an App Connect `appUrl`'s same-origin check,
|
|
127
|
+
* a wallet-onboarding `host`'s http(s)-only check). Throws a caller-supplied
|
|
128
|
+
* message on a parse failure (with the parse error as `cause`) or on a
|
|
129
|
+
* fragment, so each site keeps its own wording while the parse-and-no-
|
|
130
|
+
* fragment logic is written once.
|
|
131
|
+
*
|
|
132
|
+
* The fragment check reads the serialized URL rather than `url.hash`: a bare
|
|
133
|
+
* trailing `#` sets an empty (non-null) fragment that `hash` reports as `''`,
|
|
134
|
+
* and a percent-encoded `%23` never appears as `#` in the serialization.
|
|
135
|
+
*
|
|
136
|
+
* @param options {object}
|
|
137
|
+
* @param options.value {string} the raw wire value to parse
|
|
138
|
+
* @param options.notAbsoluteMessage {string} thrown, with the parse error
|
|
139
|
+
* as `cause`, when `value` does not parse as an absolute URL
|
|
140
|
+
* @param options.fragmentMessage {string} thrown when the parsed URL
|
|
141
|
+
* carries a fragment
|
|
142
|
+
* @returns {URL} the parsed URL
|
|
143
|
+
*/
|
|
144
|
+
export function parsedAbsoluteUrl({ value, notAbsoluteMessage, fragmentMessage }) {
|
|
145
|
+
let url;
|
|
146
|
+
try {
|
|
147
|
+
url = new URL(value);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
throw new Error(notAbsoluteMessage, { cause: err });
|
|
151
|
+
}
|
|
152
|
+
if (url.href.includes('#')) {
|
|
153
|
+
throw new Error(fragmentMessage);
|
|
154
|
+
}
|
|
155
|
+
return url;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=queryPredicates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryPredicates.js","sourceRoot":"","sources":["../src/queryPredicates.ts"],"names":[],"mappings":"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"}
|