mppx 0.14.0 → 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 +20 -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/CloseSupervisor.d.ts +13 -1
- package/dist/tempo/session/server/CloseSupervisor.js +23 -2
- package/dist/tempo/session/server/Ws.d.ts +5 -2
- package/dist/tempo/session/server/Ws.js +11 -2
- package/dist/tempo/session/server/index.d.ts +3 -1
- package/dist/tempo/session/server/index.js +2 -0
- 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,6 +1,6 @@
|
|
|
1
1
|
import { type Hex } from 'viem';
|
|
2
2
|
import * as Chain from '../precompile/Chain.js';
|
|
3
|
-
import { type OnSessionSettlementFailure, type SessionStoreInput, type SettlementTransactionOptions } from './Settlement.js';
|
|
3
|
+
import { type OnSessionSettlementFailure, type ResolvedSettlementSchedule, type SessionStoreInput, type SettlementTransactionOptions } from './Settlement.js';
|
|
4
4
|
/** Channels whose escrow states one call reads, bounding its calldata and response size. */
|
|
5
5
|
export declare const defaultBatchSize = 50;
|
|
6
6
|
/** Widest log query `rpc.tempo.xyz` accepts; wider ones fail with "query exceeds max block range". */
|
|
@@ -15,6 +15,8 @@ export declare const defaultCloseSearchMarginBlocks = 2000n;
|
|
|
15
15
|
export type CloseSupervisionOutcome =
|
|
16
16
|
/** Open with no close request, or not yet open at the pinned block. */
|
|
17
17
|
'open'
|
|
18
|
+
/** Open, and settled because its spend crossed `settlementSchedule`. */
|
|
19
|
+
| 'settled'
|
|
18
20
|
/** Close requested, with nothing above the settled amount to capture; the payer's withdrawal finalizes it. */
|
|
19
21
|
| 'closing'
|
|
20
22
|
/** A close request recorded on the channel was cancelled on-chain, so the channel accepts charges again. */
|
|
@@ -45,6 +47,8 @@ export type CloseSupervision = {
|
|
|
45
47
|
closed: number;
|
|
46
48
|
/** Channels that failed. */
|
|
47
49
|
failed: number;
|
|
50
|
+
/** Open channels settled on `settlementSchedule`. */
|
|
51
|
+
settled: number;
|
|
48
52
|
/** Channels whose deposit left the escrow before their spend was captured. */
|
|
49
53
|
unrecovered: number;
|
|
50
54
|
/**
|
|
@@ -78,6 +82,14 @@ export type SuperviseClosesParameters = Pick<SettlementTransactionOptions, 'acco
|
|
|
78
82
|
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
79
83
|
/** `block` of the last `complete` supervision, which bounds close-event searches. */
|
|
80
84
|
previousBlock?: bigint | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Settles open channels whose spend since their last settlement crossed this schedule, in raw
|
|
87
|
+
* token units, with the account and fee options of closes. Servers that charge channels with
|
|
88
|
+
* `charge()` rather than through the session handler, whose own schedule runs on each
|
|
89
|
+
* charge, use it to collect spend without waiting for a close. Settlement captures only a voucher
|
|
90
|
+
* within spend. Without it, open channels are only read.
|
|
91
|
+
*/
|
|
92
|
+
settlementSchedule?: ResolvedSettlementSchedule | undefined;
|
|
81
93
|
/** Server channel store. */
|
|
82
94
|
store: SessionStoreInput;
|
|
83
95
|
};
|
|
@@ -4,7 +4,7 @@ import { VerificationFailedError } from '../../../Errors.js';
|
|
|
4
4
|
import * as Chain from '../precompile/Chain.js';
|
|
5
5
|
import { escrowAbi } from '../precompile/escrow.abi.js';
|
|
6
6
|
import * as ChannelStore from './ChannelStore.js';
|
|
7
|
-
import { assertSettlementSender, getClientAccount, reportSessionSettlementFailure, resolveChannelStore, } from './Settlement.js';
|
|
7
|
+
import { assertSettlementSender, getClientAccount, maybeSettleScheduled, reportSessionSettlementFailure, resolveChannelStore, } from './Settlement.js';
|
|
8
8
|
/** Escrow event every close and withdrawal emits, with the amount paid to the payee. */
|
|
9
9
|
const channelClosedEvent = getAbiItem({ abi: escrowAbi, name: 'ChannelClosed' });
|
|
10
10
|
/** Channels whose escrow states one call reads, bounding its calldata and response size. */
|
|
@@ -45,6 +45,7 @@ export async function superviseCloses(parameters) {
|
|
|
45
45
|
closed: 0,
|
|
46
46
|
complete: false,
|
|
47
47
|
failed: 0,
|
|
48
|
+
settled: 0,
|
|
48
49
|
supervised: 0,
|
|
49
50
|
timestamp: pinned.timestamp,
|
|
50
51
|
unrecovered: 0,
|
|
@@ -54,6 +55,7 @@ export async function superviseCloses(parameters) {
|
|
|
54
55
|
if (outcome === 'cancelled' ||
|
|
55
56
|
outcome === 'closed' ||
|
|
56
57
|
outcome === 'failed' ||
|
|
58
|
+
outcome === 'settled' ||
|
|
57
59
|
outcome === 'unrecovered')
|
|
58
60
|
supervision[outcome] += 1;
|
|
59
61
|
supervision.channels.push({ channelId, outcome, ...details });
|
|
@@ -172,7 +174,7 @@ async function superviseChannel(parameters, store, channel, state, blocks) {
|
|
|
172
174
|
: current);
|
|
173
175
|
if (state.closeRequestedAt === 0) {
|
|
174
176
|
if (channel.closeRequestedAt === 0n)
|
|
175
|
-
return
|
|
177
|
+
return settleDue(parameters, store, channel);
|
|
176
178
|
// A pinned read without a request means the payer cancelled it, unless the record holds a
|
|
177
179
|
// server close that may still land: those expire before they are cleared.
|
|
178
180
|
const cleared = {};
|
|
@@ -198,6 +200,25 @@ async function superviseChannel(parameters, store, channel, state, blocks) {
|
|
|
198
200
|
return { outcome: 'finalized' };
|
|
199
201
|
return closeAtSpend(parameters, store, closing, state);
|
|
200
202
|
}
|
|
203
|
+
/** Settles an open channel when its spend crossed the settlement schedule. */
|
|
204
|
+
async function settleDue(parameters, store, channel) {
|
|
205
|
+
const { client, settlementSchedule } = parameters;
|
|
206
|
+
if (!settlementSchedule)
|
|
207
|
+
return { outcome: 'open' };
|
|
208
|
+
const txHash = await maybeSettleScheduled({
|
|
209
|
+
account: parameters.account ?? getClientAccount(client),
|
|
210
|
+
channel,
|
|
211
|
+
client,
|
|
212
|
+
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
213
|
+
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
214
|
+
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
215
|
+
onSessionSettlement: parameters.onSessionSettlement,
|
|
216
|
+
onSessionSettlementFailure: parameters.onSessionSettlementFailure,
|
|
217
|
+
schedule: settlementSchedule,
|
|
218
|
+
store,
|
|
219
|
+
});
|
|
220
|
+
return txHash ? { outcome: 'settled', txHash } : { outcome: 'open' };
|
|
221
|
+
}
|
|
201
222
|
/** Sends a payee close capturing the channel's spend and finalizes the channel. */
|
|
202
223
|
async function closeAtSpend(parameters, store, channel, state) {
|
|
203
224
|
const { client } = parameters;
|
|
@@ -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
|
|
@@ -9,6 +9,8 @@ export type { ChargeOptions, CreditDeclineReason, CreditOptions, CreditResult, S
|
|
|
9
9
|
export type { Parameters as SponsorshipLimitParameters } from './SponsorshipLimit.js';
|
|
10
10
|
/** SSE helpers and types for Tempo session streams. */
|
|
11
11
|
export * as Sse from './Sse.js';
|
|
12
|
-
/** Server-side automatic settlement schedule. */
|
|
12
|
+
/** Server-side automatic settlement schedule, and its raw-unit form `superviseCloses` takes. */
|
|
13
|
+
export { resolveSettlementSchedule } from './Settlement.js';
|
|
14
|
+
export type { ResolvedSettlementSchedule } from './Settlement.js';
|
|
13
15
|
export type { OnSessionSettlement, OnSessionSettlementFailure, ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, SessionSettlementContext, SessionSettlementFailureContext, SettlementSchedule, } from './Session.js';
|
|
14
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,4 +5,6 @@ export { superviseCloses } from './CloseSupervisor.js';
|
|
|
5
5
|
export { IdempotencyKeyConflictError, resolveSettleableVoucher } from './ChannelStore.js';
|
|
6
6
|
/** SSE helpers and types for Tempo session streams. */
|
|
7
7
|
export * as Sse from './Sse.js';
|
|
8
|
+
/** Server-side automatic settlement schedule, and its raw-unit form `superviseCloses` takes. */
|
|
9
|
+
export { resolveSettlementSchedule } from './Settlement.js';
|
|
8
10
|
//# sourceMappingURL=index.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 & {
|