@daski/x402-scheme 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 +21 -0
- package/README.md +81 -0
- package/dist/binding.d.ts +37 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +74 -0
- package/dist/binding.js.map +1 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +53 -0
- package/dist/canonical.js.map +1 -0
- package/dist/eip712.d.ts +72 -0
- package/dist/eip712.d.ts.map +1 -0
- package/dist/eip712.js +61 -0
- package/dist/eip712.js.map +1 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions.d.ts +29 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +65 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +177 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +345 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/policy.d.ts +113 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +357 -0
- package/dist/policy.js.map +1 -0
- package/dist/recipe.d.ts +40 -0
- package/dist/recipe.d.ts.map +1 -0
- package/dist/recipe.js +61 -0
- package/dist/recipe.js.map +1 -0
- package/dist/register.d.ts +36 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +12 -0
- package/dist/register.js.map +1 -0
- package/dist/scheme.d.ts +92 -0
- package/dist/scheme.d.ts.map +1 -0
- package/dist/scheme.js +182 -0
- package/dist/scheme.js.map +1 -0
- package/dist/signRequest.d.ts +30 -0
- package/dist/signRequest.d.ts.map +1 -0
- package/dist/signRequest.js +83 -0
- package/dist/signRequest.js.map +1 -0
- package/dist/signer.d.ts +38 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +12 -0
- package/dist/signer.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `@daski/x402-scheme`
|
|
2
|
+
|
|
3
|
+
A composite Exact-EVM client plugin for the modular x402 v2 SDK, and the
|
|
4
|
+
policy validator that decides whether a challenge may be signed at all.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @daski/x402-scheme @x402/core @x402/evm
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## It wraps, it does not replace
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { x402Client } from "@x402/core/client";
|
|
14
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
15
|
+
import { registerDaskiExactEvmScheme } from "@daski/x402-scheme";
|
|
16
|
+
|
|
17
|
+
const client = new x402Client();
|
|
18
|
+
registerDaskiExactEvmScheme(client, {
|
|
19
|
+
network: "eip155:84532",
|
|
20
|
+
signer, // your SignerAdapter
|
|
21
|
+
payerAddress,
|
|
22
|
+
policy, // PolicyConfig — no config, no signing
|
|
23
|
+
stock: new ExactEvmScheme(account), // wrapped, not discarded
|
|
24
|
+
resolvePurchaseContext, // what you're buying, what you approved
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
On `PaymentRequired`:
|
|
29
|
+
|
|
30
|
+
- **`extensions["daski-order-binding"]` present** → the Daski path: validate,
|
|
31
|
+
recompute, sign.
|
|
32
|
+
- **absent** → the wrapped stock handler, called with the same requirements and
|
|
33
|
+
context objects, by reference, untouched.
|
|
34
|
+
|
|
35
|
+
The scheme name stays `"exact"`, because that is the only name the facilitator
|
|
36
|
+
knows — inventing a second one would make the payments unverifiable.
|
|
37
|
+
`findDefaultAsset` and `schemeHooks` are forwarded, so registering the
|
|
38
|
+
composite does not quietly disable the host's spend controls.
|
|
39
|
+
|
|
40
|
+
## The policy validator
|
|
41
|
+
|
|
42
|
+
`PolicyConfig` is required. No config, no signing.
|
|
43
|
+
|
|
44
|
+
Every expectation is config- or catalog-sourced; nothing is read from the
|
|
45
|
+
challenge being validated. Before any purchase authorization is signed:
|
|
46
|
+
|
|
47
|
+
1. Chain and verifying contract equal the pinned profile values
|
|
48
|
+
2. `types` is exactly the closed 6-field `TransferWithAuthorization`
|
|
49
|
+
3. `message.from` is the configured payer
|
|
50
|
+
4. `message.to` is corroborated by **two** independent catalog sources
|
|
51
|
+
5. The amount matches the challenge, the approved quote, and the caps
|
|
52
|
+
6. The validity window is sane, and does not outlive the binding
|
|
53
|
+
7. The payment identifier is unused
|
|
54
|
+
8. (Lifecycle) the action URI and request hash are **recomputed**, not accepted
|
|
55
|
+
|
|
56
|
+
Failures raise `PolicyRefusal`, carrying the failed check, a stable code, and
|
|
57
|
+
a remediation. The validator never repairs a payload and proceeds.
|
|
58
|
+
|
|
59
|
+
## Recipe recomputation
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { recipeNonceV2 } from "@daski/x402-scheme";
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The authorization nonce is a commitment to the whole deal — chain, token,
|
|
66
|
+
payer, splitter, amount, and the five deal hashes. Recomputing it locally is
|
|
67
|
+
what lets you sign a server-proposed authorization without trusting the
|
|
68
|
+
server. When the gateway supplies `daski-sign-request`, its proposal is
|
|
69
|
+
treated as an input: recomputed, compared, and refused on mismatch.
|
|
70
|
+
|
|
71
|
+
Both `recipeNonce` (v1) and `recipeNonceV2` are implemented and pinned by
|
|
72
|
+
tests against a vector produced independently by the reference client behind
|
|
73
|
+
43 settled sandbox orders.
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
[`examples/fetch`](../../examples/fetch) · [`examples/mcp`](../../examples/mcp)
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `daski-order-binding` challenge extension: parsed as a closed shape.
|
|
3
|
+
*
|
|
4
|
+
* "Closed" is load-bearing. An extra field in a binding is a field the bridge
|
|
5
|
+
* did not check and did not feed into the recipe recomputation, so a binding
|
|
6
|
+
* that carries one is refused rather than trimmed.
|
|
7
|
+
*/
|
|
8
|
+
import type { Hex } from "viem";
|
|
9
|
+
export interface OrderBindingV1 {
|
|
10
|
+
version: 1;
|
|
11
|
+
profile: "recipe-bound-v1";
|
|
12
|
+
listingManifestHash: Hex;
|
|
13
|
+
providerOfferHash: Hex;
|
|
14
|
+
quoteHash: Hex;
|
|
15
|
+
canonicalRequestHash: Hex;
|
|
16
|
+
orderNonce: Hex;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
}
|
|
19
|
+
/** Catalog-driven checkout: the runtime listing commitment plus the
|
|
20
|
+
* provider's signed registration intent. */
|
|
21
|
+
export interface OrderBindingV2 {
|
|
22
|
+
version: 2;
|
|
23
|
+
profile: "recipe-bound-v2";
|
|
24
|
+
runtimeCommitmentHash: Hex;
|
|
25
|
+
providerIntentHash: Hex;
|
|
26
|
+
quoteHash: Hex;
|
|
27
|
+
canonicalRequestHash: Hex;
|
|
28
|
+
orderNonce: Hex;
|
|
29
|
+
expiresAt: number;
|
|
30
|
+
}
|
|
31
|
+
export type OrderBinding = OrderBindingV1 | OrderBindingV2;
|
|
32
|
+
/**
|
|
33
|
+
* Parses the `daski-order-binding` extension, or returns `undefined` when the
|
|
34
|
+
* challenge carries none (the stock-delegation path).
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseOrderBinding(value: unknown): OrderBinding | undefined;
|
|
37
|
+
//# sourceMappingURL=binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAKhC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;IAC3B,mBAAmB,EAAE,GAAG,CAAC;IACzB,iBAAiB,EAAE,GAAG,CAAC;IACvB,SAAS,EAAE,GAAG,CAAC;IACf,oBAAoB,EAAE,GAAG,CAAC;IAC1B,UAAU,EAAE,GAAG,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;6CAC6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;IAC3B,qBAAqB,EAAE,GAAG,CAAC;IAC3B,kBAAkB,EAAE,GAAG,CAAC;IACxB,SAAS,EAAE,GAAG,CAAC;IACf,oBAAoB,EAAE,GAAG,CAAC;IAC1B,UAAU,EAAE,GAAG,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG,cAAc,CAAC;AAS3D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CA8D1E"}
|
package/dist/binding.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { refuse } from "./errors.js";
|
|
2
|
+
const HEX32 = /^0x[0-9a-fA-F]{64}$/;
|
|
3
|
+
const SHARED_SLOTS = ["quoteHash", "canonicalRequestHash", "orderNonce"];
|
|
4
|
+
const DEAL_SLOTS_V1 = ["listingManifestHash", "providerOfferHash"];
|
|
5
|
+
const DEAL_SLOTS_V2 = ["runtimeCommitmentHash", "providerIntentHash"];
|
|
6
|
+
const BINDING_DOC = "https://github.com/daski-io/buyer/blob/main/docs/policy.md#daski-order-binding";
|
|
7
|
+
/**
|
|
8
|
+
* Parses the `daski-order-binding` extension, or returns `undefined` when the
|
|
9
|
+
* challenge carries none (the stock-delegation path).
|
|
10
|
+
*/
|
|
11
|
+
export function parseOrderBinding(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15
|
+
refuse({
|
|
16
|
+
check: "challenge-shape",
|
|
17
|
+
code: "DASKI_BINDING_NOT_AN_OBJECT",
|
|
18
|
+
expected: "an object under extensions['daski-order-binding']",
|
|
19
|
+
actual: Array.isArray(value) ? "an array" : typeof value,
|
|
20
|
+
remediation: `Request a fresh challenge; see ${BINDING_DOC}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const binding = value;
|
|
24
|
+
const isV2 = binding.profile === "recipe-bound-v2";
|
|
25
|
+
const dealSlots = isV2 ? DEAL_SLOTS_V2 : DEAL_SLOTS_V1;
|
|
26
|
+
const expectedKeys = [
|
|
27
|
+
"version", "profile", ...dealSlots, ...SHARED_SLOTS, "expiresAt",
|
|
28
|
+
].sort();
|
|
29
|
+
const actualKeys = Object.keys(binding).sort();
|
|
30
|
+
if (actualKeys.join(",") !== expectedKeys.join(",")) {
|
|
31
|
+
refuse({
|
|
32
|
+
check: "challenge-shape",
|
|
33
|
+
code: "DASKI_BINDING_OPEN_SHAPE",
|
|
34
|
+
expected: `exactly [${expectedKeys.join(", ")}]`,
|
|
35
|
+
actual: `[${actualKeys.join(", ")}]`,
|
|
36
|
+
remediation: "An unrecognized binding layout cannot be recomputed, so it cannot be " +
|
|
37
|
+
`signed. Upgrade @daski/x402-scheme, or see ${BINDING_DOC}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const versionOk = isV2
|
|
41
|
+
? binding.version === 2
|
|
42
|
+
: binding.version === 1 && binding.profile === "recipe-bound-v1";
|
|
43
|
+
if (!versionOk) {
|
|
44
|
+
refuse({
|
|
45
|
+
check: "challenge-shape",
|
|
46
|
+
code: "DASKI_BINDING_UNKNOWN_PROFILE",
|
|
47
|
+
expected: "recipe-bound-v1 (version 1) or recipe-bound-v2 (version 2)",
|
|
48
|
+
actual: `profile=${String(binding.profile)} version=${String(binding.version)}`,
|
|
49
|
+
remediation: `Upgrade @daski/x402-scheme; see ${BINDING_DOC}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const slot of [...dealSlots, ...SHARED_SLOTS]) {
|
|
53
|
+
if (!HEX32.test(String(binding[slot]))) {
|
|
54
|
+
refuse({
|
|
55
|
+
check: "challenge-shape",
|
|
56
|
+
code: "DASKI_BINDING_MALFORMED_SLOT",
|
|
57
|
+
expected: `${slot} to be a 32-byte hex string`,
|
|
58
|
+
actual: `${slot}=${String(binding[slot])}`,
|
|
59
|
+
remediation: `Request a fresh challenge; see ${BINDING_DOC}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isSafeInteger(binding.expiresAt) || Number(binding.expiresAt) <= 0) {
|
|
64
|
+
refuse({
|
|
65
|
+
check: "challenge-shape",
|
|
66
|
+
code: "DASKI_BINDING_MALFORMED_EXPIRY",
|
|
67
|
+
expected: "expiresAt to be a positive unix timestamp in seconds",
|
|
68
|
+
actual: `expiresAt=${String(binding.expiresAt)}`,
|
|
69
|
+
remediation: `Request a fresh challenge; see ${BINDING_DOC}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return binding;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.js","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON, byte-identical to the gateway's.
|
|
3
|
+
*
|
|
4
|
+
* Object keys sort, numbers must be finite, lone surrogates are rejected, and
|
|
5
|
+
* `undefined` is an error rather than an omission. The bridge uses this to
|
|
6
|
+
* recompute the `requestHash` in a lifecycle challenge instead of trusting
|
|
7
|
+
* the one it was handed.
|
|
8
|
+
*/
|
|
9
|
+
import { type Hex } from "viem";
|
|
10
|
+
export declare const canonicalJson: (value: unknown) => string;
|
|
11
|
+
export declare const canonicalHash: (value: unknown) => Hex;
|
|
12
|
+
//# sourceMappingURL=canonical.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAA0B,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAsCxD,eAAO,MAAM,aAAa,UAAW,OAAO,KAAG,MAA+B,CAAC;AAC/E,eAAO,MAAM,aAAa,UAAW,OAAO,KAAG,GAAoD,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON, byte-identical to the gateway's.
|
|
3
|
+
*
|
|
4
|
+
* Object keys sort, numbers must be finite, lone surrogates are rejected, and
|
|
5
|
+
* `undefined` is an error rather than an omission. The bridge uses this to
|
|
6
|
+
* recompute the `requestHash` in a lifecycle challenge instead of trusting
|
|
7
|
+
* the one it was handed.
|
|
8
|
+
*/
|
|
9
|
+
import { keccak256, stringToHex } from "viem";
|
|
10
|
+
function assertValidUnicode(value) {
|
|
11
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
12
|
+
const code = value.charCodeAt(index);
|
|
13
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
14
|
+
const next = value.charCodeAt(index + 1);
|
|
15
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
16
|
+
throw new Error("Canonical JSON contains invalid Unicode");
|
|
17
|
+
}
|
|
18
|
+
index += 1;
|
|
19
|
+
}
|
|
20
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
21
|
+
throw new Error("Canonical JSON contains invalid Unicode");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function canonicalValue(value) {
|
|
26
|
+
if (value === null)
|
|
27
|
+
return "null";
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
assertValidUnicode(value);
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "boolean")
|
|
33
|
+
return JSON.stringify(value);
|
|
34
|
+
if (typeof value === "number") {
|
|
35
|
+
if (!Number.isFinite(value))
|
|
36
|
+
throw new Error("Canonical JSON accepts only finite numbers");
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return `[${value.map(canonicalValue).join(",")}]`;
|
|
41
|
+
if (!value || typeof value !== "object")
|
|
42
|
+
throw new Error("Unsupported canonical value");
|
|
43
|
+
const object = value;
|
|
44
|
+
return `{${Object.keys(object).sort().map((key) => {
|
|
45
|
+
assertValidUnicode(key);
|
|
46
|
+
if (object[key] === undefined)
|
|
47
|
+
throw new Error("Undefined canonical value");
|
|
48
|
+
return `${JSON.stringify(key)}:${canonicalValue(object[key])}`;
|
|
49
|
+
}).join(",")}}`;
|
|
50
|
+
}
|
|
51
|
+
export const canonicalJson = (value) => canonicalValue(value);
|
|
52
|
+
export const canonicalHash = (value) => keccak256(stringToHex(canonicalValue(value)));
|
|
53
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.js","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAY,MAAM,MAAM,CAAC;AAExD,SAAS,kBAAkB,CAAC,KAAa;IACvC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YACzC,IAAI,CAAC,CAAC,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;YAC7D,CAAC;YACD,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YAC5C,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAC3F,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC5E,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACxF,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QAChD,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAC5E,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IACjE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAO,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/eip712.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one typed-data shape this package will ever sign for a purchase.
|
|
3
|
+
*
|
|
4
|
+
* `TransferWithAuthorization` is declared here as a closed literal and
|
|
5
|
+
* compared field-for-field against whatever the server proposes. The bridge
|
|
6
|
+
* never signs a server-supplied `types` object: it signs *this* one, after
|
|
7
|
+
* confirming the server asked for exactly this one.
|
|
8
|
+
*/
|
|
9
|
+
import type { Address, Hex } from "viem";
|
|
10
|
+
export declare const TRANSFER_WITH_AUTHORIZATION_PRIMARY_TYPE: "TransferWithAuthorization";
|
|
11
|
+
/** The closed 6-field EIP-3009 type set. Order is part of the hash. */
|
|
12
|
+
export declare const TRANSFER_WITH_AUTHORIZATION_TYPES: {
|
|
13
|
+
readonly TransferWithAuthorization: readonly [{
|
|
14
|
+
readonly name: "from";
|
|
15
|
+
readonly type: "address";
|
|
16
|
+
}, {
|
|
17
|
+
readonly name: "to";
|
|
18
|
+
readonly type: "address";
|
|
19
|
+
}, {
|
|
20
|
+
readonly name: "value";
|
|
21
|
+
readonly type: "uint256";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "validAfter";
|
|
24
|
+
readonly type: "uint256";
|
|
25
|
+
}, {
|
|
26
|
+
readonly name: "validBefore";
|
|
27
|
+
readonly type: "uint256";
|
|
28
|
+
}, {
|
|
29
|
+
readonly name: "nonce";
|
|
30
|
+
readonly type: "bytes32";
|
|
31
|
+
}];
|
|
32
|
+
};
|
|
33
|
+
/** The EIP-3009 authorization as it travels on the wire: decimal strings. */
|
|
34
|
+
export interface TransferAuthorization {
|
|
35
|
+
from: Address;
|
|
36
|
+
to: Address;
|
|
37
|
+
value: string;
|
|
38
|
+
validAfter: string;
|
|
39
|
+
validBefore: string;
|
|
40
|
+
nonce: Hex;
|
|
41
|
+
}
|
|
42
|
+
export interface Eip712Domain {
|
|
43
|
+
name: string;
|
|
44
|
+
version: string;
|
|
45
|
+
chainId: number;
|
|
46
|
+
verifyingContract: Address;
|
|
47
|
+
}
|
|
48
|
+
/** A complete typed-data request, in the shape a signer adapter receives. */
|
|
49
|
+
export interface TypedDataRequest {
|
|
50
|
+
domain: Eip712Domain;
|
|
51
|
+
types: Record<string, readonly {
|
|
52
|
+
name: string;
|
|
53
|
+
type: string;
|
|
54
|
+
}[]>;
|
|
55
|
+
primaryType: string;
|
|
56
|
+
message: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds the exact typed-data payload for an authorization. `value`,
|
|
60
|
+
* `validAfter` and `validBefore` become bigints here because that is what
|
|
61
|
+
* EIP-712 encoding requires; the wire form keeps them as strings.
|
|
62
|
+
*/
|
|
63
|
+
export declare function transferWithAuthorizationTypedData(domain: Eip712Domain, authorization: TransferAuthorization): TypedDataRequest;
|
|
64
|
+
/**
|
|
65
|
+
* True when `types` is exactly the closed 6-field set: same type names, same
|
|
66
|
+
* field names, same field types, same order, and nothing extra at either
|
|
67
|
+
* level. Used by §4.1 check 2.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isClosedTransferWithAuthorizationTypes(types: unknown): boolean;
|
|
70
|
+
/** The exact field set an EIP-3009 authorization message may carry. */
|
|
71
|
+
export declare const AUTHORIZATION_FIELDS: readonly ["from", "to", "value", "validAfter", "validBefore", "nonce"];
|
|
72
|
+
//# sourceMappingURL=eip712.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eip712.d.ts","sourceRoot":"","sources":["../src/eip712.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEzC,eAAO,MAAM,wCAAwC,EAAG,2BAAoC,CAAC;AAE7F,uEAAuE;AACvE,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;CASpC,CAAC;AAEX,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,OAAO,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,CAChD,MAAM,EAAE,YAAY,EACpB,aAAa,EAAE,qBAAqB,GACnC,gBAAgB,CAclB;AAED;;;;GAIG;AACH,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAe9E;AAED,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,wEAEvB,CAAC"}
|
package/dist/eip712.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const TRANSFER_WITH_AUTHORIZATION_PRIMARY_TYPE = "TransferWithAuthorization";
|
|
2
|
+
/** The closed 6-field EIP-3009 type set. Order is part of the hash. */
|
|
3
|
+
export const TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
4
|
+
TransferWithAuthorization: [
|
|
5
|
+
{ name: "from", type: "address" },
|
|
6
|
+
{ name: "to", type: "address" },
|
|
7
|
+
{ name: "value", type: "uint256" },
|
|
8
|
+
{ name: "validAfter", type: "uint256" },
|
|
9
|
+
{ name: "validBefore", type: "uint256" },
|
|
10
|
+
{ name: "nonce", type: "bytes32" },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Builds the exact typed-data payload for an authorization. `value`,
|
|
15
|
+
* `validAfter` and `validBefore` become bigints here because that is what
|
|
16
|
+
* EIP-712 encoding requires; the wire form keeps them as strings.
|
|
17
|
+
*/
|
|
18
|
+
export function transferWithAuthorizationTypedData(domain, authorization) {
|
|
19
|
+
return {
|
|
20
|
+
domain,
|
|
21
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
22
|
+
primaryType: TRANSFER_WITH_AUTHORIZATION_PRIMARY_TYPE,
|
|
23
|
+
message: {
|
|
24
|
+
from: authorization.from,
|
|
25
|
+
to: authorization.to,
|
|
26
|
+
value: BigInt(authorization.value),
|
|
27
|
+
validAfter: BigInt(authorization.validAfter),
|
|
28
|
+
validBefore: BigInt(authorization.validBefore),
|
|
29
|
+
nonce: authorization.nonce,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True when `types` is exactly the closed 6-field set: same type names, same
|
|
35
|
+
* field names, same field types, same order, and nothing extra at either
|
|
36
|
+
* level. Used by §4.1 check 2.
|
|
37
|
+
*/
|
|
38
|
+
export function isClosedTransferWithAuthorizationTypes(types) {
|
|
39
|
+
if (!types || typeof types !== "object" || Array.isArray(types))
|
|
40
|
+
return false;
|
|
41
|
+
const record = types;
|
|
42
|
+
// EIP712Domain is derived from the domain, never accepted as a declared type.
|
|
43
|
+
if (Object.keys(record).join(",") !== TRANSFER_WITH_AUTHORIZATION_PRIMARY_TYPE)
|
|
44
|
+
return false;
|
|
45
|
+
const fields = record[TRANSFER_WITH_AUTHORIZATION_PRIMARY_TYPE];
|
|
46
|
+
const expected = TRANSFER_WITH_AUTHORIZATION_TYPES.TransferWithAuthorization;
|
|
47
|
+
if (!Array.isArray(fields) || fields.length !== expected.length)
|
|
48
|
+
return false;
|
|
49
|
+
return expected.every((want, index) => {
|
|
50
|
+
const got = fields[index];
|
|
51
|
+
return !!got && typeof got === "object" && !Array.isArray(got) &&
|
|
52
|
+
Object.keys(got).sort().join(",") === "name,type" &&
|
|
53
|
+
got.name === want.name &&
|
|
54
|
+
got.type === want.type;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** The exact field set an EIP-3009 authorization message may carry. */
|
|
58
|
+
export const AUTHORIZATION_FIELDS = [
|
|
59
|
+
"from", "to", "value", "validAfter", "validBefore", "nonce",
|
|
60
|
+
];
|
|
61
|
+
//# sourceMappingURL=eip712.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eip712.js","sourceRoot":"","sources":["../src/eip712.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,MAAM,wCAAwC,GAAG,2BAAoC,CAAC;AAE7F,uEAAuE;AACvE,MAAM,CAAC,MAAM,iCAAiC,GAAG;IAC/C,yBAAyB,EAAE;QACzB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;QACjC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE;QAC/B,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;QAClC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE;QACvC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE;QACxC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;KACnC;CACO,CAAC;AA2BX;;;;GAIG;AACH,MAAM,UAAU,kCAAkC,CAChD,MAAoB,EACpB,aAAoC;IAEpC,OAAO;QACL,MAAM;QACN,KAAK,EAAE,iCAAiC;QACxC,WAAW,EAAE,wCAAwC;QACrD,OAAO,EAAE;YACP,IAAI,EAAE,aAAa,CAAC,IAAI;YACxB,EAAE,EAAE,aAAa,CAAC,EAAE;YACpB,KAAK,EAAE,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC;YAClC,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC;YAC5C,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC;YAC9C,KAAK,EAAE,aAAa,CAAC,KAAK;SAC3B;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sCAAsC,CAAC,KAAc;IACnE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,8EAA8E;IAC9E,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,wCAAwC;QAAE,OAAO,KAAK,CAAC;IAC7F,MAAM,MAAM,GAAG,MAAM,CAAC,wCAAwC,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,iCAAiC,CAAC,yBAAyB,CAAC;IAC7E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC9E,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACpC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,CAAC,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC,GAAa,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,WAAW;YAC1D,GAAyB,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;YAC5C,GAAyB,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO;CACnD,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every refusal this package raises is structured: a stable machine code, the
|
|
3
|
+
* §4 check that failed, and the remediation a human can act on. Nothing here
|
|
4
|
+
* ever carries key material — see `redact.ts` for the outbound guard.
|
|
5
|
+
*/
|
|
6
|
+
/** The §4.1 checks, in spec order, plus the lifecycle and recompute tiers. */
|
|
7
|
+
export type PolicyCheck = "chain-pinning" | "typed-data-shape" | "payer-match" | "splitter-allowlist" | "amount-and-caps" | "authorization-window" | "payment-identifier" | "lifecycle-binding" | "recipe-recompute" | "challenge-shape";
|
|
8
|
+
export interface PolicyRefusalDetail {
|
|
9
|
+
/** Which §4.1 assertion refused. */
|
|
10
|
+
check: PolicyCheck;
|
|
11
|
+
/** Stable machine-readable code, e.g. `DASKI_POLICY_SPLITTER_NOT_ALLOWLISTED`. */
|
|
12
|
+
code: string;
|
|
13
|
+
/** What the bridge required. */
|
|
14
|
+
expected?: string;
|
|
15
|
+
/** What the server proposed. Never key material. */
|
|
16
|
+
actual?: string;
|
|
17
|
+
/** The exact command or doc URL that resolves this. */
|
|
18
|
+
remediation: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A refusal to sign. The bridge never "fixes and proceeds": a failed check
|
|
22
|
+
* ends the attempt, and the caller is told which check and what to do.
|
|
23
|
+
*/
|
|
24
|
+
export declare class PolicyRefusal extends Error {
|
|
25
|
+
readonly name = "PolicyRefusal";
|
|
26
|
+
readonly detail: PolicyRefusalDetail;
|
|
27
|
+
constructor(detail: PolicyRefusalDetail);
|
|
28
|
+
toJSON(): PolicyRefusalDetail & {
|
|
29
|
+
error: "policy_refusal";
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function refuse(detail: PolicyRefusalDetail): never;
|
|
34
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GACnB,eAAe,GACf,kBAAkB,GAClB,aAAa,GACb,oBAAoB,GACpB,iBAAiB,GACjB,sBAAsB,GACtB,oBAAoB,GACpB,mBAAmB,GACnB,kBAAkB,GAClB,iBAAiB,CAAC;AAEtB,MAAM,WAAW,mBAAmB;IAClC,oCAAoC;IACpC,KAAK,EAAE,WAAW,CAAC;IACnB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAkB,IAAI,mBAAmB;IACzC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;gBAEzB,MAAM,EAAE,mBAAmB;IAKvC,MAAM,IAAI,mBAAmB,GAAG;QAAE,KAAK,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;CAG7E;AASD,wBAAgB,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,KAAK,CAEzD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every refusal this package raises is structured: a stable machine code, the
|
|
3
|
+
* §4 check that failed, and the remediation a human can act on. Nothing here
|
|
4
|
+
* ever carries key material — see `redact.ts` for the outbound guard.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A refusal to sign. The bridge never "fixes and proceeds": a failed check
|
|
8
|
+
* ends the attempt, and the caller is told which check and what to do.
|
|
9
|
+
*/
|
|
10
|
+
export class PolicyRefusal extends Error {
|
|
11
|
+
name = "PolicyRefusal";
|
|
12
|
+
detail;
|
|
13
|
+
constructor(detail) {
|
|
14
|
+
super(`${detail.code}: ${refusalSentence(detail)}`);
|
|
15
|
+
this.detail = detail;
|
|
16
|
+
}
|
|
17
|
+
toJSON() {
|
|
18
|
+
return { error: "policy_refusal", message: this.message, ...this.detail };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function refusalSentence(detail) {
|
|
22
|
+
const parts = [`the ${detail.check} check refused this challenge`];
|
|
23
|
+
if (detail.expected !== undefined)
|
|
24
|
+
parts.push(`expected ${detail.expected}`);
|
|
25
|
+
if (detail.actual !== undefined)
|
|
26
|
+
parts.push(`server proposed ${detail.actual}`);
|
|
27
|
+
return parts.join("; ");
|
|
28
|
+
}
|
|
29
|
+
export function refuse(detail) {
|
|
30
|
+
throw new PolicyRefusal(detail);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA4BH;;;GAGG;AACH,MAAM,OAAO,aAAc,SAAQ,KAAK;IACpB,IAAI,GAAG,eAAe,CAAC;IAChC,MAAM,CAAsB;IAErC,YAAY,MAA2B;QACrC,KAAK,CAAC,GAAG,MAAM,CAAC,IAAI,KAAK,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC5E,CAAC;CACF;AAED,SAAS,eAAe,CAAC,MAA2B;IAClD,MAAM,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,KAAK,+BAA+B,CAAC,CAAC;IACnE,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC7E,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAChF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,MAA2B;IAChD,MAAM,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const DASKI_ORDER_BINDING = "daski-order-binding";
|
|
2
|
+
export declare const DASKI_SIGN_REQUEST = "daski-sign-request";
|
|
3
|
+
export declare const PAYMENT_IDENTIFIER = "payment-identifier";
|
|
4
|
+
/** Alphanumeric, hyphen and underscore, 16–128 characters. */
|
|
5
|
+
export declare const PAYMENT_IDENTIFIER_PATTERN: RegExp;
|
|
6
|
+
/**
|
|
7
|
+
* The extension set a paid submission echoes: every issued extension except
|
|
8
|
+
* `bazaar` and `daski-sign-request`.
|
|
9
|
+
*
|
|
10
|
+
* `bazaar` is dropped because its inlined outcome schemas can push the encoded
|
|
11
|
+
* `PAYMENT-SIGNATURE` header past the inbound caps in front of the gateway
|
|
12
|
+
* (Node's 16 KiB default, Cloudflare's per-header limit), which would refuse
|
|
13
|
+
* the paid retry with a 431 before the gateway ever saw it. The gateway
|
|
14
|
+
* verifies a payment that omits `bazaar` and hash-checks it when echoed.
|
|
15
|
+
*
|
|
16
|
+
* `daski-sign-request` is dropped because it is the server's *proposal*, not
|
|
17
|
+
* part of the deal: echoing it back would assert we agreed to a document we
|
|
18
|
+
* only used as an input to our own recomputation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function paymentEchoExtensions(issued: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Adds our payment identifier to the issued `payment-identifier` extension.
|
|
23
|
+
* The identifier is the reconciliation key: it is what makes an interrupted
|
|
24
|
+
* purchase provably findable instead of ambiguously re-signable.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withPaymentIdentifier(extensions: Record<string, unknown>, identifier: string | undefined): Record<string, unknown>;
|
|
27
|
+
/** Reads the identifier the server pinned, if it pinned one. */
|
|
28
|
+
export declare function issuedPaymentIdentifier(extensions: Record<string, unknown> | undefined): string | undefined;
|
|
29
|
+
//# sourceMappingURL=extensions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AACzD,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B,QAA4B,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC1C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB;AAED,gEAAgE;AAChE,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC9C,MAAM,GAAG,SAAS,CAKpB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Challenge extensions: what we echo back with a paid submission, and what we
|
|
3
|
+
* refuse to echo.
|
|
4
|
+
*/
|
|
5
|
+
import { refuse } from "./errors.js";
|
|
6
|
+
export const DASKI_ORDER_BINDING = "daski-order-binding";
|
|
7
|
+
export const DASKI_SIGN_REQUEST = "daski-sign-request";
|
|
8
|
+
export const PAYMENT_IDENTIFIER = "payment-identifier";
|
|
9
|
+
/** Alphanumeric, hyphen and underscore, 16–128 characters. */
|
|
10
|
+
export const PAYMENT_IDENTIFIER_PATTERN = /^[a-zA-Z0-9_-]{16,128}$/;
|
|
11
|
+
/**
|
|
12
|
+
* The extension set a paid submission echoes: every issued extension except
|
|
13
|
+
* `bazaar` and `daski-sign-request`.
|
|
14
|
+
*
|
|
15
|
+
* `bazaar` is dropped because its inlined outcome schemas can push the encoded
|
|
16
|
+
* `PAYMENT-SIGNATURE` header past the inbound caps in front of the gateway
|
|
17
|
+
* (Node's 16 KiB default, Cloudflare's per-header limit), which would refuse
|
|
18
|
+
* the paid retry with a 431 before the gateway ever saw it. The gateway
|
|
19
|
+
* verifies a payment that omits `bazaar` and hash-checks it when echoed.
|
|
20
|
+
*
|
|
21
|
+
* `daski-sign-request` is dropped because it is the server's *proposal*, not
|
|
22
|
+
* part of the deal: echoing it back would assert we agreed to a document we
|
|
23
|
+
* only used as an input to our own recomputation.
|
|
24
|
+
*/
|
|
25
|
+
export function paymentEchoExtensions(issued) {
|
|
26
|
+
const { bazaar: _discovery, [DASKI_SIGN_REQUEST]: _proposal, ...extensions } = issued ?? {};
|
|
27
|
+
return extensions;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Adds our payment identifier to the issued `payment-identifier` extension.
|
|
31
|
+
* The identifier is the reconciliation key: it is what makes an interrupted
|
|
32
|
+
* purchase provably findable instead of ambiguously re-signable.
|
|
33
|
+
*/
|
|
34
|
+
export function withPaymentIdentifier(extensions, identifier) {
|
|
35
|
+
if (identifier === undefined)
|
|
36
|
+
return extensions;
|
|
37
|
+
if (!PAYMENT_IDENTIFIER_PATTERN.test(identifier)) {
|
|
38
|
+
refuse({
|
|
39
|
+
check: "payment-identifier",
|
|
40
|
+
code: "DASKI_PAYMENT_IDENTIFIER_MALFORMED",
|
|
41
|
+
expected: "16-128 characters of [A-Za-z0-9_-]",
|
|
42
|
+
actual: `identifier of length ${identifier.length}`,
|
|
43
|
+
remediation: "Let @daski/pay generate the identifier, or supply one matching the " +
|
|
44
|
+
"documented pattern.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const issued = extensions[PAYMENT_IDENTIFIER];
|
|
48
|
+
if (issued === undefined)
|
|
49
|
+
return extensions;
|
|
50
|
+
const declaration = issued;
|
|
51
|
+
return {
|
|
52
|
+
...extensions,
|
|
53
|
+
[PAYMENT_IDENTIFIER]: {
|
|
54
|
+
...declaration,
|
|
55
|
+
info: { ...(declaration.info ?? {}), id: identifier },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Reads the identifier the server pinned, if it pinned one. */
|
|
60
|
+
export function issuedPaymentIdentifier(extensions) {
|
|
61
|
+
const issued = extensions?.[PAYMENT_IDENTIFIER];
|
|
62
|
+
const id = issued?.info?.id;
|
|
63
|
+
return typeof id === "string" ? id : undefined;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=extensions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions.js","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AACzD,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,8DAA8D;AAC9D,MAAM,CAAC,MAAM,0BAA0B,GAAG,yBAAyB,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAA2C;IAE3C,MAAM,EACJ,MAAM,EAAE,UAAU,EAClB,CAAC,kBAAkB,CAAC,EAAE,SAAS,EAC/B,GAAG,UAAU,EACd,GAAG,MAAM,IAAI,EAAE,CAAC;IACjB,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAmC,EACnC,UAA8B;IAE9B,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAChD,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACjD,MAAM,CAAC;YACL,KAAK,EAAE,oBAAoB;YAC3B,IAAI,EAAE,oCAAoC;YAC1C,QAAQ,EAAE,oCAAoC;YAC9C,MAAM,EAAE,wBAAwB,UAAU,CAAC,MAAM,EAAE;YACnD,WAAW,EACT,qEAAqE;gBACrE,qBAAqB;SACxB,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAC5C,MAAM,WAAW,GAAG,MAA4C,CAAC;IACjE,OAAO;QACL,GAAG,UAAU;QACb,CAAC,kBAAkB,CAAC,EAAE;YACpB,GAAG,WAAW;YACd,IAAI,EAAE,EAAE,GAAG,CAAC,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE;SACtD;KACF,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,uBAAuB,CACrC,UAA+C;IAE/C,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC,kBAAkB,CACL,CAAC;IAC1C,MAAM,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;IAC5B,OAAO,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACjD,CAAC"}
|