mppx 0.14.1 → 0.15.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 +14 -0
- package/README.md +1 -1
- package/dist/Expires.js +1 -1
- package/dist/Store.d.ts +2 -2
- package/dist/Store.js +13 -3
- package/dist/attestation/internal/HttpMessageSignature.js +2 -0
- package/dist/cli/cli.js +25 -25
- package/dist/cli/plugins/index.js +2 -2
- package/dist/integrations/x402/Evm.d.ts +1 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/middlewares/express.js +18 -6
- package/dist/server/Mppx.d.ts +5 -0
- package/dist/server/Mppx.js +8 -3
- package/dist/tempo/Methods.d.ts +5 -0
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/client/Subscription.d.ts +5 -0
- package/dist/tempo/server/Charge.js +4 -0
- package/dist/tempo/server/Methods.d.ts +26 -0
- package/dist/tempo/server/Subscription.d.ts +5 -0
- package/dist/tempo/server/Subscription.js +8 -4
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/server/Ws.d.ts +5 -2
- package/dist/tempo/session/server/Ws.js +11 -2
- package/dist/tempo/subscription/Store.d.ts +7 -1
- package/dist/tempo/subscription/Store.js +56 -4
- package/dist/tempo/subscription/Types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as Challenge from '../../../Challenge.js';
|
|
1
2
|
import { type SessionCredentialContext } from '../precompile/Protocol.js';
|
|
2
3
|
import * as ChannelStore from './ChannelStore.js';
|
|
3
4
|
import type { MeteredStreamOptions, SessionController } from './MeteredStream.js';
|
|
@@ -13,6 +14,7 @@ export type SessionRouteResult = {
|
|
|
13
14
|
challenge: Response;
|
|
14
15
|
} | {
|
|
15
16
|
status: 200;
|
|
17
|
+
challenge?: Challenge.Challenge | undefined;
|
|
16
18
|
withReceipt(response?: Response): Response | Promise<Response>;
|
|
17
19
|
};
|
|
18
20
|
/** HTTP route used to perform a WebSocket payment probe. */
|
|
@@ -36,14 +38,15 @@ export declare function serve(options: serve.Options): Promise<void>;
|
|
|
36
38
|
/** Type helpers for {@link serve}. */
|
|
37
39
|
export declare namespace serve {
|
|
38
40
|
type Options = {
|
|
39
|
-
/** Expected per-tick amount in raw units.
|
|
41
|
+
/** Expected per-tick amount in raw units. Required for custom routes that
|
|
42
|
+
* do not return their canonical challenge. When set, credentials whose
|
|
40
43
|
* challenge `request.amount` does not match are rejected. Use this to
|
|
41
44
|
* pin the price when the route is backed by `Mppx.compose()` with
|
|
42
45
|
* multiple offers — otherwise a client can select the cheapest offer
|
|
43
46
|
* and still receive the same stream. */
|
|
44
47
|
amount?: string | undefined;
|
|
45
48
|
/** Application stream. A manual stream can call `charge(amount)` with a
|
|
46
|
-
* per-message raw-unit amount; omitting it uses the
|
|
49
|
+
* per-message raw-unit amount; omitting it uses the canonical route tick cost. */
|
|
47
50
|
generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>);
|
|
48
51
|
/** Maximum accepted inbound WebSocket frame size in bytes. @default 65536 */
|
|
49
52
|
maxIncomingMessageBytes?: number | undefined;
|
|
@@ -133,6 +133,11 @@ export async function serve(options) {
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
const { receipt } = authorizationResult;
|
|
136
|
+
const tickCost = expectedAmount !== undefined ? BigInt(expectedAmount) : authorizationResult.amount;
|
|
137
|
+
if (tickCost === undefined) {
|
|
138
|
+
await close(1008, 'WebSocket route must provide canonical pricing or an explicit amount');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
136
141
|
if (payload.action === 'voucher' && runtime.streamContext) {
|
|
137
142
|
runtime.streamContext.challengeId = credential.challenge.id;
|
|
138
143
|
}
|
|
@@ -149,7 +154,7 @@ export async function serve(options) {
|
|
|
149
154
|
runtime.streamContext = {
|
|
150
155
|
challengeId: credential.challenge.id,
|
|
151
156
|
channelId: payload.channelId,
|
|
152
|
-
tickCost
|
|
157
|
+
tickCost,
|
|
153
158
|
};
|
|
154
159
|
// Defer the first application frame until after the client receives the
|
|
155
160
|
// auth receipt and has a chance to install its own message listeners.
|
|
@@ -299,6 +304,10 @@ async function authorizePaymentFrame(parameters) {
|
|
|
299
304
|
const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt);
|
|
300
305
|
if (!receiptHeader)
|
|
301
306
|
throw new Error('management response missing Payment-Receipt header');
|
|
302
|
-
return {
|
|
307
|
+
return {
|
|
308
|
+
status: 'accepted',
|
|
309
|
+
amount: result.challenge ? readSessionChallengeAmount(result.challenge) : undefined,
|
|
310
|
+
receipt: deserializeSessionReceipt(receiptHeader),
|
|
311
|
+
};
|
|
303
312
|
}
|
|
304
313
|
//# sourceMappingURL=Ws.js.map
|
|
@@ -13,9 +13,13 @@ export type SubscriptionStore = {
|
|
|
13
13
|
/** Looks up a generated access key for a resolved request key. */
|
|
14
14
|
getAccessKey(key: string): Promise<SubscriptionAccessKeyRecord | null>;
|
|
15
15
|
/** Looks up a generated access key by its public access key address. */
|
|
16
|
-
getAccessKeyByAddress(address: string): Promise<SubscriptionAccessKeyRecord | null>;
|
|
16
|
+
getAccessKeyByAddress(address: string, keyToken?: `0x${string}`): Promise<SubscriptionAccessKeyRecord | null>;
|
|
17
17
|
/** Looks up the active subscription for a resolved request key. */
|
|
18
18
|
getByKey(key: string): Promise<SubscriptionRecord | null>;
|
|
19
|
+
/** Creates a key sealed in a client-carried token, without retaining per-challenge state. */
|
|
20
|
+
createChallengeAccessKey(): Promise<SubscriptionAccessKeyRecord>;
|
|
21
|
+
/** Retains a key after verified payment submission for subsequent subscription billing. */
|
|
22
|
+
retainAccessKey(key: string, record: SubscriptionAccessKeyRecord): Promise<void>;
|
|
19
23
|
/** Gets or creates the server-owned access key for a resolved request key. */
|
|
20
24
|
getOrCreateAccessKey(key: string): Promise<SubscriptionAccessKeyRecord>;
|
|
21
25
|
/** Upserts a subscription record and marks it as active for its lookup key. */
|
|
@@ -85,6 +89,8 @@ export declare namespace fromStore {
|
|
|
85
89
|
type Options = {
|
|
86
90
|
/** Key prefix for server-owned subscription access keys. @default `'tempo:subscription:access-key:'` */
|
|
87
91
|
accessKeyPrefix?: string | undefined;
|
|
92
|
+
/** Namespace for the persistent anonymous-challenge sealing key. */
|
|
93
|
+
pendingAccessKeyPrefix?: string | undefined;
|
|
88
94
|
/** Key prefix for resolved subscription activation locks. @default `'tempo:subscription:activation:'` */
|
|
89
95
|
activationPrefix?: string | undefined;
|
|
90
96
|
/** Milliseconds before a stuck activation lock can be replaced. @default `900000` */
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { Secp256k1 } from 'ox';
|
|
1
|
+
import { Hex, Secp256k1 } from 'ox';
|
|
2
2
|
import { Account as TempoAccount } from 'viem/tempo';
|
|
3
3
|
const defaultRecordPrefix = 'tempo:subscription:record:';
|
|
4
4
|
const defaultKeyPrefix = 'tempo:subscription:key:';
|
|
5
5
|
const defaultActivationPrefix = 'tempo:subscription:activation:';
|
|
6
6
|
const defaultAccessKeyPrefix = 'tempo:subscription:access-key:';
|
|
7
|
+
const defaultPendingAccessKeyPrefix = 'tempo:subscription:pending-access-key:';
|
|
7
8
|
const defaultCredentialPrefix = 'tempo:subscription:credential:';
|
|
8
9
|
const defaultActivationTimeoutMs = 15 * 60 * 1_000;
|
|
9
10
|
const defaultRenewalTimeoutMs = 15 * 60 * 1_000;
|
|
10
11
|
/** Wraps a generic key-value {@link Store.Store} with subscription-specific accessors. */
|
|
11
12
|
export function fromStore(store, options) {
|
|
12
|
-
const { accessKeyPrefix = defaultAccessKeyPrefix, activationPrefix = defaultActivationPrefix, activationTimeoutMs = defaultActivationTimeoutMs, credentialPrefix = defaultCredentialPrefix, keyPrefix = defaultKeyPrefix, recordPrefix = defaultRecordPrefix, renewalTimeoutMs = defaultRenewalTimeoutMs, } = options ?? {};
|
|
13
|
+
const { accessKeyPrefix = defaultAccessKeyPrefix, activationPrefix = defaultActivationPrefix, activationTimeoutMs = defaultActivationTimeoutMs, credentialPrefix = defaultCredentialPrefix, keyPrefix = defaultKeyPrefix, recordPrefix = defaultRecordPrefix, pendingAccessKeyPrefix = defaultPendingAccessKeyPrefix, renewalTimeoutMs = defaultRenewalTimeoutMs, } = options ?? {};
|
|
13
14
|
function recordKey(subscriptionId) {
|
|
14
15
|
return `${recordPrefix}${subscriptionId}`;
|
|
15
16
|
}
|
|
@@ -61,7 +62,35 @@ export function fromStore(store, options) {
|
|
|
61
62
|
const marker = (await store.get(activationKey(lookupKey)));
|
|
62
63
|
return marker?.challengeId === challengeId;
|
|
63
64
|
}
|
|
65
|
+
const sealingKey = `${pendingAccessKeyPrefix}sealing-key`;
|
|
66
|
+
// One shared secret survives across workers; anonymous issuance never grows the store.
|
|
67
|
+
async function getSealingKey() {
|
|
68
|
+
const candidate = Hex.fromBytes(crypto.getRandomValues(new Uint8Array(32)));
|
|
69
|
+
const secret = await store.update(sealingKey, (current) => current
|
|
70
|
+
? { op: 'noop', result: current }
|
|
71
|
+
: { op: 'set', value: candidate, result: candidate });
|
|
72
|
+
return crypto.subtle.importKey('raw', new Uint8Array(Hex.toBytes(secret)), 'AES-GCM', false, [
|
|
73
|
+
'encrypt',
|
|
74
|
+
'decrypt',
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
64
77
|
return {
|
|
78
|
+
async createChallengeAccessKey() {
|
|
79
|
+
const privateKey = Secp256k1.randomPrivateKey();
|
|
80
|
+
const account = TempoAccount.fromSecp256k1(privateKey);
|
|
81
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
82
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, await getSealingKey(), new Uint8Array(Hex.toBytes(privateKey))));
|
|
83
|
+
return {
|
|
84
|
+
accessKeyAddress: account.address.toLowerCase(),
|
|
85
|
+
keyType: account.keyType,
|
|
86
|
+
privateKey,
|
|
87
|
+
keyToken: Hex.fromBytes(new Uint8Array([...iv, ...sealed])),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
async retainAccessKey(key, record) {
|
|
91
|
+
await store.put(accessKeyKey(key), record);
|
|
92
|
+
await store.put(accessKeyAddressKey(record.accessKeyAddress), record);
|
|
93
|
+
},
|
|
65
94
|
async activate({ challengeId, create, isReusable, lookupKey }) {
|
|
66
95
|
const claimed = await store.update(credentialKey(challengeId), (current) => {
|
|
67
96
|
if (current)
|
|
@@ -155,8 +184,31 @@ export function fromStore(store, options) {
|
|
|
155
184
|
async getAccessKey(key) {
|
|
156
185
|
return (await store.get(accessKeyKey(key)));
|
|
157
186
|
},
|
|
158
|
-
async getAccessKeyByAddress(address) {
|
|
159
|
-
|
|
187
|
+
async getAccessKeyByAddress(address, keyToken) {
|
|
188
|
+
const stored = (await store.get(accessKeyAddressKey(address)));
|
|
189
|
+
if (stored)
|
|
190
|
+
return stored;
|
|
191
|
+
if (!keyToken)
|
|
192
|
+
return null;
|
|
193
|
+
try {
|
|
194
|
+
const bytes = Hex.toBytes(keyToken);
|
|
195
|
+
// 12-byte nonce, 32-byte private key, and 16-byte authentication tag.
|
|
196
|
+
if (bytes.length !== 60)
|
|
197
|
+
return null;
|
|
198
|
+
const privateKey = Hex.fromBytes(new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bytes.slice(0, 12) }, await getSealingKey(), bytes.slice(12))));
|
|
199
|
+
const account = TempoAccount.fromSecp256k1(privateKey);
|
|
200
|
+
if (account.address.toLowerCase() !== address.toLowerCase())
|
|
201
|
+
return null;
|
|
202
|
+
return {
|
|
203
|
+
accessKeyAddress: account.address.toLowerCase(),
|
|
204
|
+
keyType: account.keyType,
|
|
205
|
+
privateKey,
|
|
206
|
+
keyToken,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
160
212
|
},
|
|
161
213
|
async getByKey(key) {
|
|
162
214
|
return getByLookupKey(key);
|
|
@@ -5,6 +5,8 @@ export type SubscriptionPeriodUnit = 'dev_second' | 'day' | 'week';
|
|
|
5
5
|
export type SubscriptionAccessKey = {
|
|
6
6
|
accessKeyAddress: Address;
|
|
7
7
|
keyType: 'p256' | 'secp256k1' | 'webAuthn';
|
|
8
|
+
/** Opaque server-sealed key for stateless anonymous setup. */
|
|
9
|
+
keyToken?: `0x${string}` | undefined;
|
|
8
10
|
};
|
|
9
11
|
/** Server-owned subscription access key persisted for automatic billing. */
|
|
10
12
|
export type SubscriptionAccessKeyRecord = SubscriptionAccessKey & {
|