@forgezero/runtime 0.1.24 → 0.1.26
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/README.md +31 -7
- package/dist/custody-share.d.ts +87 -2
- package/dist/custody-share.js +162 -4
- package/dist/operation.d.ts +64 -0
- package/dist/operation.js +107 -0
- package/dist/realtime.d.ts +86 -4
- package/dist/realtime.js +163 -14
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# Platform runtime
|
|
9
9
|
|
|
10
|
-
The machinery behind a request handler — jobs, queues, an outbox, a hash-chained audit trail, mail, backups, schema validation and exact money.
|
|
10
|
+
The machinery behind a request handler — jobs, queues, an outbox, a hash-chained audit trail, mail, backups, schema validation and exact money. 33 public modules, each imported on its own.
|
|
11
11
|
|
|
12
12
|
## Package overview
|
|
13
13
|
|
|
@@ -62,6 +62,7 @@ These are supported consumer entry points, not every internal module shipped for
|
|
|
62
62
|
| @forgezero/runtime/finance/tax | Which jurisdiction may tax a sale, who accounts for it, and the three ways to be zero that report differently. Rates are data. | portable | [Reference + usage](#forgezero-runtime-finance-tax) |
|
|
63
63
|
| @forgezero/runtime/finance/storage | Write an amount so it is both exact and sortable: an authoritative string, and a number that is only an index. | portable | [Reference + usage](#forgezero-runtime-finance-storage) |
|
|
64
64
|
| @forgezero/runtime/realtime | Provider-neutral realtime audience, shard, event and delivery contracts. | portable | [Reference + usage](#forgezero-runtime-realtime) |
|
|
65
|
+
| @forgezero/runtime/operation | Revisioned surface, operation-state and bounded redacted operation-log contracts. | portable | [Reference + usage](#forgezero-runtime-operation) |
|
|
65
66
|
| @forgezero/runtime/passkey-hybrid | Versioned WebAuthn PRF plus ML-DSA companion proof construction and verification. | portable | [Reference + usage](#forgezero-runtime-passkey-hybrid) |
|
|
66
67
|
| @forgezero/runtime/otpauth | Parse and render otpauth URIs without binding enrolment to a UI framework. | portable | [Reference + usage](#forgezero-runtime-otpauth) |
|
|
67
68
|
| @forgezero/runtime/pipeline | Typed ordered application-pipeline execution with explicit evidence. | portable | [Reference + usage](#forgezero-runtime-pipeline) |
|
|
@@ -722,7 +723,7 @@ Provider-neutral realtime audience, shard, event and delivery contracts. This is
|
|
|
722
723
|
|
|
723
724
|
```text
|
|
724
725
|
import {
|
|
725
|
-
|
|
726
|
+
REALTIME_FLUSH_BATCH_BYTES,
|
|
726
727
|
} from '@forgezero/runtime/realtime';
|
|
727
728
|
```
|
|
728
729
|
|
|
@@ -732,10 +733,33 @@ This minimal executable use imports one concrete value from this exact entry poi
|
|
|
732
733
|
|
|
733
734
|
```text
|
|
734
735
|
import {
|
|
735
|
-
|
|
736
|
+
REALTIME_FLUSH_BATCH_BYTES,
|
|
736
737
|
} from '@forgezero/runtime/realtime';
|
|
737
738
|
|
|
738
|
-
export const selectedCapability =
|
|
739
|
+
export const selectedCapability = REALTIME_FLUSH_BATCH_BYTES;
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
<a id="forgezero-runtime-operation"></a>
|
|
743
|
+
## @forgezero/runtime/operation
|
|
744
|
+
|
|
745
|
+
Revisioned surface, operation-state and bounded redacted operation-log contracts. This is a supported entry point. Import only the named values used by the adjacent task example; the declaration file remains the complete API reference.
|
|
746
|
+
|
|
747
|
+
```text
|
|
748
|
+
import {
|
|
749
|
+
OPERATION_MAX_EVENTS,
|
|
750
|
+
} from '@forgezero/runtime/operation';
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
## @forgezero/runtime/operation — Use this entry point
|
|
754
|
+
|
|
755
|
+
This minimal executable use imports one concrete value from this exact entry point without a wildcard or package-root detour. Keep the value or values the application actually needs; the full named inventory remains directly above it.
|
|
756
|
+
|
|
757
|
+
```text
|
|
758
|
+
import {
|
|
759
|
+
OPERATION_MAX_EVENTS,
|
|
760
|
+
} from '@forgezero/runtime/operation';
|
|
761
|
+
|
|
762
|
+
export const selectedCapability = OPERATION_MAX_EVENTS;
|
|
739
763
|
```
|
|
740
764
|
|
|
741
765
|
<a id="forgezero-runtime-passkey-hybrid"></a>
|
|
@@ -814,7 +838,7 @@ Threshold-share parsing, validation and reconstruction. This is a supported entr
|
|
|
814
838
|
|
|
815
839
|
```text
|
|
816
840
|
import {
|
|
817
|
-
|
|
841
|
+
CUSTODY_ENVELOPE_SUITE,
|
|
818
842
|
} from '@forgezero/runtime/custody-share';
|
|
819
843
|
```
|
|
820
844
|
|
|
@@ -824,10 +848,10 @@ This minimal executable use imports one concrete value from this exact entry poi
|
|
|
824
848
|
|
|
825
849
|
```text
|
|
826
850
|
import {
|
|
827
|
-
|
|
851
|
+
CUSTODY_ENVELOPE_SUITE,
|
|
828
852
|
} from '@forgezero/runtime/custody-share';
|
|
829
853
|
|
|
830
|
-
export const selectedCapability =
|
|
854
|
+
export const selectedCapability = CUSTODY_ENVELOPE_SUITE;
|
|
831
855
|
```
|
|
832
856
|
|
|
833
857
|
<a id="forgezero-runtime-custody-crypto"></a>
|
package/dist/custody-share.d.ts
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
1
|
import { type CipherBox, type SealedToKey } from './custody-crypto';
|
|
2
|
+
export declare const CUSTODY_ENVELOPE_VERSION: 3;
|
|
3
|
+
export declare const CUSTODY_ENVELOPE_SUITE: "ml-kem-768+x25519+hkdf-sha256+aes-256-gcm+ml-dsa-65";
|
|
4
|
+
export declare const CUSTODY_FACTOR_PROOF_VERSION: 1;
|
|
5
|
+
export type CustodyCredentialPurpose = 'custody';
|
|
6
|
+
export type CustodyCredentialLifecycle = 'pending' | 'active' | 'retired';
|
|
7
|
+
export type CustodyFactor = 'ceremony_passkey' | 'recovery_phrase';
|
|
8
|
+
export type CustodyEnvelopeGeneration = number;
|
|
9
|
+
export type CustodyTransactionOutcome = 'completed' | 'factor_mismatch' | 'credential_mismatch' | 'replayed' | 'expired' | 'batch_mismatch' | 'seed_fingerprint_mismatch';
|
|
10
|
+
export interface CustodyFactorBinding {
|
|
11
|
+
version: typeof CUSTODY_FACTOR_PROOF_VERSION;
|
|
12
|
+
realmId: string;
|
|
13
|
+
ceremonyKey: string;
|
|
14
|
+
custodianKey: string;
|
|
15
|
+
factor: CustodyFactor;
|
|
16
|
+
requestKey: string;
|
|
17
|
+
method: 'POST';
|
|
18
|
+
path: string;
|
|
19
|
+
payloadDigest: string;
|
|
20
|
+
challenge: string;
|
|
21
|
+
expiresAtSec: number;
|
|
22
|
+
}
|
|
23
|
+
export interface CustodyPasskeyProofV1 {
|
|
24
|
+
version: typeof CUSTODY_FACTOR_PROOF_VERSION;
|
|
25
|
+
factor: 'ceremony_passkey';
|
|
26
|
+
credentialId: string;
|
|
27
|
+
signature: string;
|
|
28
|
+
}
|
|
29
|
+
export interface CustodyPhraseProofV1 {
|
|
30
|
+
version: typeof CUSTODY_FACTOR_PROOF_VERSION;
|
|
31
|
+
factor: 'recovery_phrase';
|
|
32
|
+
signature: string;
|
|
33
|
+
}
|
|
34
|
+
export type CustodyFactorProof = CustodyPasskeyProofV1 | CustodyPhraseProofV1;
|
|
35
|
+
export interface CustodyActionRequest {
|
|
36
|
+
_key: string;
|
|
37
|
+
ceremonyKey: string;
|
|
38
|
+
ownerUserKey: string;
|
|
39
|
+
operation: 'seal' | 'verify' | 'reshare' | 'unlock' | 'rotate' | 'verify_rotation';
|
|
40
|
+
allowedFactors: CustodyFactor[];
|
|
41
|
+
payloadDigest: string;
|
|
42
|
+
challenge: string;
|
|
43
|
+
status: 'open' | 'consumed' | 'expired';
|
|
44
|
+
expiresAtSec: number;
|
|
45
|
+
revision: number;
|
|
46
|
+
}
|
|
2
47
|
export interface SealedShare {
|
|
3
48
|
shareIndex: number;
|
|
4
|
-
/**
|
|
49
|
+
/** Dedicated custody credential whose PRF deterministically derives this passkey key. */
|
|
5
50
|
passkeyCredentialId?: string;
|
|
51
|
+
/** Base64url ceremony salt persisted with that exact custody credential. */
|
|
52
|
+
passkeyPrfSalt?: string;
|
|
6
53
|
passkeyEnvelope: CipherBox;
|
|
7
54
|
phraseEnvelope: CipherBox;
|
|
8
55
|
/** base64 — the HKDF salt for the phrase key and the verifier. */
|
|
@@ -10,6 +57,22 @@ export interface SealedShare {
|
|
|
10
57
|
/** hex — proves the right phrase without being able to rebuild it. */
|
|
11
58
|
phraseVerifier: string;
|
|
12
59
|
}
|
|
60
|
+
/** New writes use this shape. V2 is retained only for bounded unlock-and-rotate migration. */
|
|
61
|
+
export interface CustodyEnvelopeV3 extends SealedShare {
|
|
62
|
+
version: typeof CUSTODY_ENVELOPE_VERSION;
|
|
63
|
+
suite: typeof CUSTODY_ENVELOPE_SUITE;
|
|
64
|
+
generation: CustodyEnvelopeGeneration;
|
|
65
|
+
passkeyGeneration?: CustodyEnvelopeGeneration;
|
|
66
|
+
phraseGeneration?: CustodyEnvelopeGeneration;
|
|
67
|
+
realmId: string;
|
|
68
|
+
ceremonyKey: string;
|
|
69
|
+
custodianKey: string;
|
|
70
|
+
passkeyCredentialId: string;
|
|
71
|
+
passkeyPrfSalt: string;
|
|
72
|
+
passkeyActionPublicKey: string;
|
|
73
|
+
phraseActionPublicKey: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function isCustodyEnvelopeV3(value: SealedShare): value is CustodyEnvelopeV3;
|
|
13
76
|
type EnvelopeKind = 'passkey' | 'phrase';
|
|
14
77
|
/** The x-coordinate a share was cut at — its custodian-facing share number. */
|
|
15
78
|
export declare function shareIndexOf(share: Uint8Array): number;
|
|
@@ -44,12 +107,15 @@ export declare function openShareWithPhrase(sealed: SealedShare, custodianKey: s
|
|
|
44
107
|
* ever sent.
|
|
45
108
|
*/
|
|
46
109
|
export interface WrappingKeys {
|
|
47
|
-
/** The verified, PRF-capable
|
|
110
|
+
/** The verified, PRF-capable custody credential selected by the browser. */
|
|
48
111
|
passkeyCredentialId: string;
|
|
112
|
+
passkeyPrfSalt: string;
|
|
49
113
|
passkeyPublicKey: string;
|
|
50
114
|
phrasePublicKey: string;
|
|
51
115
|
phraseSalt: string;
|
|
52
116
|
phraseVerifier: string;
|
|
117
|
+
passkeyActionPublicKey: string;
|
|
118
|
+
phraseActionPublicKey: string;
|
|
53
119
|
}
|
|
54
120
|
/** The private half for one factor. Never leaves the machine that made it. */
|
|
55
121
|
export declare function passkeyWrappingKey(custodianKey: string, passkeyPrfOutput: Uint8Array): {
|
|
@@ -65,9 +131,25 @@ export declare function phraseWrappingKey(custodianKey: string, phraseWords: str
|
|
|
65
131
|
export declare function wrappingKeysFor(args: {
|
|
66
132
|
custodianKey: string;
|
|
67
133
|
passkeyCredentialId: string;
|
|
134
|
+
passkeyPrfSalt: string;
|
|
68
135
|
passkeyPrfOutput: Uint8Array;
|
|
69
136
|
phraseWords: string[];
|
|
70
137
|
}): WrappingKeys;
|
|
138
|
+
export declare function custodyFactorActionPublicKey(factor: CustodyFactor, custodianKey: string, factorMaterial: Uint8Array): string;
|
|
139
|
+
/** Stable browser/API digest used to bind a custody proof to one exact payload. */
|
|
140
|
+
export declare function custodyActionPayloadDigest(value: unknown): string;
|
|
141
|
+
export declare function createCustodyFactorProof(args: {
|
|
142
|
+
binding: CustodyFactorBinding;
|
|
143
|
+
factorMaterial: Uint8Array;
|
|
144
|
+
credentialId?: string;
|
|
145
|
+
}): CustodyFactorProof;
|
|
146
|
+
export declare function createCustodyPasskeyProof(binding: CustodyFactorBinding, passkeyPrfOutput: Uint8Array, credentialId: string): CustodyPasskeyProofV1;
|
|
147
|
+
export declare function createCustodyPhraseProof(binding: CustodyFactorBinding, phraseWords: string[], phraseSalt: string): CustodyPhraseProofV1;
|
|
148
|
+
export declare function verifyCustodyFactorProof(args: {
|
|
149
|
+
binding: CustodyFactorBinding;
|
|
150
|
+
proof: CustodyFactorProof;
|
|
151
|
+
publicKey: string;
|
|
152
|
+
}): boolean;
|
|
71
153
|
/** Open something the server sealed to one of those public keys. */
|
|
72
154
|
export declare function openSealedToFactor(args: {
|
|
73
155
|
custodianKey: string;
|
|
@@ -76,9 +158,12 @@ export declare function openSealedToFactor(args: {
|
|
|
76
158
|
passkeyPrfOutput?: Uint8Array;
|
|
77
159
|
phraseWords?: string[];
|
|
78
160
|
phraseSalt?: string;
|
|
161
|
+
aad?: string;
|
|
79
162
|
}): Uint8Array;
|
|
80
163
|
/** The aad the server must use when sealing to a custodian's factor key. */
|
|
81
164
|
export declare const factorWrapAad: (custodianKey: string, factor: EnvelopeKind) => string;
|
|
165
|
+
/** Canonical AAD for V3. Length-prefixing prevents ambiguous concatenation. */
|
|
166
|
+
export declare function custodyEnvelopeV3Aad(envelope: Pick<CustodyEnvelopeV3, 'version' | 'suite' | 'realmId' | 'ceremonyKey' | 'custodianKey' | 'shareIndex' | 'passkeyCredentialId' | 'generation' | 'passkeyGeneration' | 'phraseGeneration'>, factor: EnvelopeKind): string;
|
|
82
167
|
/**
|
|
83
168
|
* A share plus a probe, sealed together.
|
|
84
169
|
*
|
package/dist/custody-share.js
CHANGED
|
@@ -174,9 +174,18 @@ function phraseVerifier(words, salt) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// src/custody-share.ts
|
|
177
|
+
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
|
|
178
|
+
import { hkdf as hkdf3 } from "@noble/hashes/hkdf.js";
|
|
179
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
180
|
+
var CUSTODY_ENVELOPE_VERSION = 3;
|
|
181
|
+
var CUSTODY_ENVELOPE_SUITE = "ml-kem-768+x25519+hkdf-sha256+aes-256-gcm+ml-dsa-65";
|
|
182
|
+
var CUSTODY_FACTOR_PROOF_VERSION = 1;
|
|
177
183
|
var KEY_BYTES3 = 32;
|
|
178
184
|
var MIN_PRF_BYTES = 32;
|
|
179
185
|
var PASSKEY_KEY_SALT = "forgezero:custodian:passkey:v1";
|
|
186
|
+
function isCustodyEnvelopeV3(value) {
|
|
187
|
+
return value.version === CUSTODY_ENVELOPE_VERSION && value.suite === CUSTODY_ENVELOPE_SUITE;
|
|
188
|
+
}
|
|
180
189
|
var utf82 = (value) => new TextEncoder().encode(value);
|
|
181
190
|
var toBase642 = (bytes) => {
|
|
182
191
|
let binary = "";
|
|
@@ -283,31 +292,167 @@ function phraseWrappingKey(custodianKey, phraseWords, salt) {
|
|
|
283
292
|
function wrappingKeysFor(args) {
|
|
284
293
|
if (!args.passkeyCredentialId)
|
|
285
294
|
throw new Error("custody-share: passkey credential id is required");
|
|
295
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(args.passkeyPrfSalt)) {
|
|
296
|
+
throw new Error("custody-share: ceremony passkey PRF salt is required");
|
|
297
|
+
}
|
|
286
298
|
const salt = newSalt();
|
|
287
299
|
const passkey = passkeyWrappingKey(args.custodianKey, args.passkeyPrfOutput);
|
|
288
300
|
const phrase = phraseWrappingKey(args.custodianKey, args.phraseWords, salt);
|
|
301
|
+
const passkeyAction = custodyFactorActionKeyPair("ceremony_passkey", args.custodianKey, args.passkeyPrfOutput);
|
|
302
|
+
const phraseMaterial = phraseToKey(args.phraseWords, salt);
|
|
303
|
+
const phraseAction = custodyFactorActionKeyPair("recovery_phrase", args.custodianKey, phraseMaterial);
|
|
289
304
|
try {
|
|
290
305
|
return {
|
|
291
306
|
passkeyCredentialId: args.passkeyCredentialId,
|
|
307
|
+
passkeyPrfSalt: args.passkeyPrfSalt,
|
|
292
308
|
passkeyPublicKey: toBase642(passkey.publicKey),
|
|
293
309
|
phrasePublicKey: toBase642(phrase.publicKey),
|
|
294
310
|
phraseSalt: toBase642(salt),
|
|
295
|
-
phraseVerifier: phraseVerifier(args.phraseWords, salt)
|
|
311
|
+
phraseVerifier: phraseVerifier(args.phraseWords, salt),
|
|
312
|
+
passkeyActionPublicKey: toBase642(passkeyAction.publicKey),
|
|
313
|
+
phraseActionPublicKey: toBase642(phraseAction.publicKey)
|
|
296
314
|
};
|
|
297
315
|
} finally {
|
|
298
316
|
passkey.secretKey.fill(0);
|
|
299
317
|
phrase.secretKey.fill(0);
|
|
318
|
+
passkeyAction.secretKey.fill(0);
|
|
319
|
+
phraseAction.secretKey.fill(0);
|
|
320
|
+
phraseMaterial.fill(0);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
var actionInfo = (factor, custodianKey) => `forgezero:custody:factor-action:ml-dsa-65:v1:${factor}:${custodianKey.length}:${custodianKey}`;
|
|
324
|
+
function custodyFactorActionKeyPair(factor, custodianKey, factorMaterial) {
|
|
325
|
+
assertCustodianKey(custodianKey);
|
|
326
|
+
if (factorMaterial.length < 32)
|
|
327
|
+
throw new Error("custody-share: action factor must be at least 32 bytes");
|
|
328
|
+
const seed = hkdf3(sha2563, factorMaterial, utf82("forgezero:custody:factor-action:ml-dsa-65:v1"), utf82(actionInfo(factor, custodianKey)), 32);
|
|
329
|
+
try {
|
|
330
|
+
const pair = ml_dsa65.keygen(seed);
|
|
331
|
+
return { publicKey: Uint8Array.from(pair.publicKey), secretKey: Uint8Array.from(pair.secretKey) };
|
|
332
|
+
} finally {
|
|
333
|
+
seed.fill(0);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function custodyFactorActionPublicKey(factor, custodianKey, factorMaterial) {
|
|
337
|
+
const pair = custodyFactorActionKeyPair(factor, custodianKey, factorMaterial);
|
|
338
|
+
try {
|
|
339
|
+
return toBase642(pair.publicKey);
|
|
340
|
+
} finally {
|
|
341
|
+
pair.secretKey.fill(0);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
var proofBytes = (binding) => {
|
|
345
|
+
const values = [
|
|
346
|
+
"forgezero-custody-factor-proof-v1",
|
|
347
|
+
String(binding.version),
|
|
348
|
+
binding.realmId,
|
|
349
|
+
binding.ceremonyKey,
|
|
350
|
+
binding.custodianKey,
|
|
351
|
+
binding.factor,
|
|
352
|
+
binding.requestKey,
|
|
353
|
+
binding.method,
|
|
354
|
+
binding.path,
|
|
355
|
+
binding.payloadDigest,
|
|
356
|
+
binding.challenge,
|
|
357
|
+
String(binding.expiresAtSec)
|
|
358
|
+
];
|
|
359
|
+
return utf82(values.map((value) => `${utf82(value).length}:${value}`).join(`
|
|
360
|
+
`));
|
|
361
|
+
};
|
|
362
|
+
function canonicalJson(value) {
|
|
363
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
364
|
+
return JSON.stringify(value);
|
|
365
|
+
if (typeof value === "number") {
|
|
366
|
+
if (!Number.isFinite(value))
|
|
367
|
+
throw new Error("custody-share: non-finite action payload number");
|
|
368
|
+
return JSON.stringify(value);
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(value))
|
|
371
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
372
|
+
if (typeof value === "object") {
|
|
373
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
374
|
+
}
|
|
375
|
+
throw new Error("custody-share: unsupported action payload value");
|
|
376
|
+
}
|
|
377
|
+
function custodyActionPayloadDigest(value) {
|
|
378
|
+
return Array.from(sha2563(utf82(canonicalJson(value))), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
379
|
+
}
|
|
380
|
+
function createCustodyFactorProof(args) {
|
|
381
|
+
if (args.binding.version !== CUSTODY_FACTOR_PROOF_VERSION) {
|
|
382
|
+
throw new Error("custody-share: unsupported factor proof version");
|
|
383
|
+
}
|
|
384
|
+
const pair = custodyFactorActionKeyPair(args.binding.factor, args.binding.custodianKey, args.factorMaterial);
|
|
385
|
+
try {
|
|
386
|
+
const signature = toBase642(ml_dsa65.sign(proofBytes(args.binding), pair.secretKey));
|
|
387
|
+
return args.binding.factor === "ceremony_passkey" ? {
|
|
388
|
+
version: CUSTODY_FACTOR_PROOF_VERSION,
|
|
389
|
+
factor: "ceremony_passkey",
|
|
390
|
+
credentialId: args.credentialId ?? "",
|
|
391
|
+
signature
|
|
392
|
+
} : { version: CUSTODY_FACTOR_PROOF_VERSION, factor: "recovery_phrase", signature };
|
|
393
|
+
} finally {
|
|
394
|
+
pair.secretKey.fill(0);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function createCustodyPasskeyProof(binding, passkeyPrfOutput, credentialId) {
|
|
398
|
+
if (binding.factor !== "ceremony_passkey") {
|
|
399
|
+
throw new Error("custody-share: passkey proof requires ceremony_passkey binding");
|
|
400
|
+
}
|
|
401
|
+
return createCustodyFactorProof({
|
|
402
|
+
binding,
|
|
403
|
+
factorMaterial: passkeyPrfOutput,
|
|
404
|
+
credentialId
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function createCustodyPhraseProof(binding, phraseWords, phraseSalt) {
|
|
408
|
+
if (binding.factor !== "recovery_phrase") {
|
|
409
|
+
throw new Error("custody-share: phrase proof requires recovery_phrase binding");
|
|
410
|
+
}
|
|
411
|
+
if (!validatePhrase(phraseWords))
|
|
412
|
+
throw new Error("INVALID_PHRASE");
|
|
413
|
+
const material = phraseToKey(phraseWords, fromBase642(phraseSalt));
|
|
414
|
+
try {
|
|
415
|
+
return createCustodyFactorProof({ binding, factorMaterial: material });
|
|
416
|
+
} finally {
|
|
417
|
+
material.fill(0);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function verifyCustodyFactorProof(args) {
|
|
421
|
+
try {
|
|
422
|
+
if (args.proof.version !== CUSTODY_FACTOR_PROOF_VERSION || args.proof.factor !== args.binding.factor)
|
|
423
|
+
return false;
|
|
424
|
+
const publicKey = fromBase642(args.publicKey);
|
|
425
|
+
const signature = fromBase642(args.proof.signature);
|
|
426
|
+
return publicKey.length === ml_dsa65.lengths.publicKey && signature.length === ml_dsa65.lengths.signature && ml_dsa65.verify(signature, proofBytes(args.binding), publicKey);
|
|
427
|
+
} catch {
|
|
428
|
+
return false;
|
|
300
429
|
}
|
|
301
430
|
}
|
|
302
431
|
function openSealedToFactor(args) {
|
|
303
432
|
const pair = args.factor === "passkey" ? passkeyWrappingKey(args.custodianKey, args.passkeyPrfOutput) : phraseWrappingKey(args.custodianKey, args.phraseWords, fromBase642(args.phraseSalt));
|
|
304
433
|
try {
|
|
305
|
-
return openFromKey(pair.secretKey, args.box, wrapInfo(args.custodianKey, args.factor));
|
|
434
|
+
return openFromKey(pair.secretKey, args.box, args.aad ?? wrapInfo(args.custodianKey, args.factor));
|
|
306
435
|
} finally {
|
|
307
436
|
pair.secretKey.fill(0);
|
|
308
437
|
}
|
|
309
438
|
}
|
|
310
439
|
var factorWrapAad = (custodianKey, factor) => wrapInfo(custodianKey, factor);
|
|
440
|
+
function custodyEnvelopeV3Aad(envelope, factor) {
|
|
441
|
+
const values = [
|
|
442
|
+
"forgezero-custody-envelope-aad",
|
|
443
|
+
String(envelope.version),
|
|
444
|
+
envelope.suite,
|
|
445
|
+
envelope.realmId,
|
|
446
|
+
envelope.ceremonyKey,
|
|
447
|
+
envelope.custodianKey,
|
|
448
|
+
String(envelope.shareIndex),
|
|
449
|
+
factor,
|
|
450
|
+
factor === "passkey" ? envelope.passkeyCredentialId : "recovery_phrase",
|
|
451
|
+
String(factor === "passkey" ? envelope.passkeyGeneration ?? envelope.generation : envelope.phraseGeneration ?? envelope.generation)
|
|
452
|
+
];
|
|
453
|
+
return values.map((value) => `${utf82(value).length}:${value}`).join(`
|
|
454
|
+
`);
|
|
455
|
+
}
|
|
311
456
|
var PROBE_BYTES = 32;
|
|
312
457
|
function joinProbeAndShare(probe, share) {
|
|
313
458
|
if (probe.length !== PROBE_BYTES)
|
|
@@ -324,17 +469,20 @@ function splitProbeAndShare(opened) {
|
|
|
324
469
|
}
|
|
325
470
|
function openFactorEnvelope(args) {
|
|
326
471
|
const box = args.factor === "passkey" ? args.sealed.passkeyEnvelope : args.sealed.phraseEnvelope;
|
|
472
|
+
const aad = isCustodyEnvelopeV3(args.sealed) ? custodyEnvelopeV3Aad(args.sealed, args.factor) : undefined;
|
|
327
473
|
return splitProbeAndShare(openSealedToFactor({
|
|
328
474
|
custodianKey: args.custodianKey,
|
|
329
475
|
factor: args.factor,
|
|
330
476
|
box,
|
|
331
477
|
passkeyPrfOutput: args.passkeyPrfOutput,
|
|
332
478
|
phraseWords: args.phraseWords,
|
|
333
|
-
phraseSalt: args.sealed.phraseSalt
|
|
479
|
+
phraseSalt: args.sealed.phraseSalt,
|
|
480
|
+
aad
|
|
334
481
|
}));
|
|
335
482
|
}
|
|
336
483
|
export {
|
|
337
484
|
wrappingKeysFor,
|
|
485
|
+
verifyCustodyFactorProof,
|
|
338
486
|
splitProbeAndShare,
|
|
339
487
|
shareIndexOf,
|
|
340
488
|
sealShare,
|
|
@@ -345,6 +493,16 @@ export {
|
|
|
345
493
|
openSealedToFactor,
|
|
346
494
|
openFactorEnvelope,
|
|
347
495
|
joinProbeAndShare,
|
|
496
|
+
isCustodyEnvelopeV3,
|
|
348
497
|
factorWrapAad,
|
|
349
|
-
|
|
498
|
+
custodyFactorActionPublicKey,
|
|
499
|
+
custodyEnvelopeV3Aad,
|
|
500
|
+
custodyActionPayloadDigest,
|
|
501
|
+
createCustodyPhraseProof,
|
|
502
|
+
createCustodyPasskeyProof,
|
|
503
|
+
createCustodyFactorProof,
|
|
504
|
+
PROBE_BYTES,
|
|
505
|
+
CUSTODY_FACTOR_PROOF_VERSION,
|
|
506
|
+
CUSTODY_ENVELOPE_VERSION,
|
|
507
|
+
CUSTODY_ENVELOPE_SUITE
|
|
350
508
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** A revisioned read model returned by one bounded application surface. */
|
|
2
|
+
export interface SurfaceSnapshot<T> {
|
|
3
|
+
revision: string;
|
|
4
|
+
generatedAtTs: number;
|
|
5
|
+
data: T;
|
|
6
|
+
}
|
|
7
|
+
export declare const OPERATION_STATUSES: readonly ["queued", "running", "stalled", "failed", "succeeded", "cancelled", "compensating", "compensated"];
|
|
8
|
+
export type OperationStatus = (typeof OPERATION_STATUSES)[number];
|
|
9
|
+
export type OperationEventOrigin = 'platform-api' | 'bootstrap-runner' | 'metal-agent' | 'compute-agent' | 'worker';
|
|
10
|
+
export interface OperationFailure {
|
|
11
|
+
code: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
retryable: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface OperationEvent {
|
|
16
|
+
sequence: number;
|
|
17
|
+
stage: string;
|
|
18
|
+
status: OperationStatus;
|
|
19
|
+
atTs: number;
|
|
20
|
+
attempt: number;
|
|
21
|
+
origin: OperationEventOrigin;
|
|
22
|
+
nodeKey?: string;
|
|
23
|
+
failure?: OperationFailure;
|
|
24
|
+
}
|
|
25
|
+
export interface OperationSnapshot {
|
|
26
|
+
operationKey: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
correlationId: string;
|
|
29
|
+
parentOperationKey?: string;
|
|
30
|
+
childOperationKeys: readonly string[];
|
|
31
|
+
stage: string;
|
|
32
|
+
status: OperationStatus;
|
|
33
|
+
attempt: number;
|
|
34
|
+
revision: number;
|
|
35
|
+
createdAtTs: number;
|
|
36
|
+
updatedAtTs: number;
|
|
37
|
+
lastHeartbeatAtTs?: number;
|
|
38
|
+
deadlineAtTs?: number;
|
|
39
|
+
leaseOwner?: string;
|
|
40
|
+
failure?: OperationFailure;
|
|
41
|
+
events: readonly OperationEvent[];
|
|
42
|
+
}
|
|
43
|
+
export interface OperationLogLine {
|
|
44
|
+
sequence: number;
|
|
45
|
+
atTs: number;
|
|
46
|
+
stream: 'stdout' | 'stderr' | 'system';
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
export interface OperationLogBatch {
|
|
50
|
+
operationKey: string;
|
|
51
|
+
fromSequence: number;
|
|
52
|
+
toSequence: number;
|
|
53
|
+
terminal: boolean;
|
|
54
|
+
lines: readonly OperationLogLine[];
|
|
55
|
+
}
|
|
56
|
+
export declare const OPERATION_MAX_EVENTS = 128;
|
|
57
|
+
export declare const OPERATION_MAX_LOG_LINES = 100;
|
|
58
|
+
export declare const OPERATION_MAX_LOG_TEXT_BYTES: number;
|
|
59
|
+
/** Redact common credential forms before an operation line reaches storage or a socket. */
|
|
60
|
+
export declare function redactOperationLogText(input: string): string;
|
|
61
|
+
export declare function validateSurfaceSnapshot<T>(input: unknown, validateData: (value: unknown) => value is T): SurfaceSnapshot<T>;
|
|
62
|
+
export declare function validateOperationFailure(input: unknown): OperationFailure;
|
|
63
|
+
export declare function validateOperationSnapshot(input: unknown): OperationSnapshot;
|
|
64
|
+
export declare function validateOperationLogBatch(input: unknown): OperationLogBatch;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined")
|
|
5
|
+
return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/operation.ts
|
|
10
|
+
var OPERATION_STATUSES = [
|
|
11
|
+
"queued",
|
|
12
|
+
"running",
|
|
13
|
+
"stalled",
|
|
14
|
+
"failed",
|
|
15
|
+
"succeeded",
|
|
16
|
+
"cancelled",
|
|
17
|
+
"compensating",
|
|
18
|
+
"compensated"
|
|
19
|
+
];
|
|
20
|
+
var ATOM = /^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/;
|
|
21
|
+
var FAILURE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
22
|
+
var OPERATION_MAX_EVENTS = 128;
|
|
23
|
+
var OPERATION_MAX_LOG_LINES = 100;
|
|
24
|
+
var OPERATION_MAX_LOG_TEXT_BYTES = 8 * 1024;
|
|
25
|
+
function redactOperationLogText(input) {
|
|
26
|
+
return input.replace(/-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/gi, "[REDACTED_PRIVATE_KEY]").replace(/\b(?:Bearer\s+)?(?:gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|fze_[A-Za-z0-9_-]{16,})\b/gi, "[REDACTED_TOKEN]").replace(/\b(password|secret|token|api[_-]?key)\s*[:=]\s*([^\s,;]+)/gi, "$1=[REDACTED]").slice(0, OPERATION_MAX_LOG_TEXT_BYTES);
|
|
27
|
+
}
|
|
28
|
+
var finiteEpoch = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
29
|
+
var atom = (value) => typeof value === "string" && ATOM.test(value);
|
|
30
|
+
function validateSurfaceSnapshot(input, validateData) {
|
|
31
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
32
|
+
throw new Error("Surface snapshot must be an object.");
|
|
33
|
+
const row = input;
|
|
34
|
+
if (!atom(row.revision) || !finiteEpoch(row.generatedAtTs) || !validateData(row.data)) {
|
|
35
|
+
throw new Error("Surface snapshot is invalid.");
|
|
36
|
+
}
|
|
37
|
+
return { revision: row.revision, generatedAtTs: row.generatedAtTs, data: row.data };
|
|
38
|
+
}
|
|
39
|
+
function validateOperationFailure(input) {
|
|
40
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
41
|
+
throw new Error("Operation failure must be an object.");
|
|
42
|
+
const row = input;
|
|
43
|
+
if (typeof row.code !== "string" || !FAILURE_CODE.test(row.code) || typeof row.detail !== "string" || !row.detail.trim() || new TextEncoder().encode(row.detail).byteLength > 2000 || typeof row.retryable !== "boolean") {
|
|
44
|
+
throw new Error("Operation failure is invalid.");
|
|
45
|
+
}
|
|
46
|
+
return { code: row.code, detail: row.detail.trim(), retryable: row.retryable };
|
|
47
|
+
}
|
|
48
|
+
function validateOperationSnapshot(input) {
|
|
49
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
50
|
+
throw new Error("Operation snapshot must be an object.");
|
|
51
|
+
const row = input;
|
|
52
|
+
if (!atom(row.operationKey) || !atom(row.kind) || !atom(row.correlationId) || row.parentOperationKey !== undefined && !atom(row.parentOperationKey) || !Array.isArray(row.childOperationKeys) || row.childOperationKeys.length > 128 || row.childOperationKeys.some((key) => !atom(key)) || !atom(row.stage) || !OPERATION_STATUSES.includes(row.status) || !Number.isSafeInteger(row.attempt) || row.attempt < 1 || !Number.isSafeInteger(row.revision) || row.revision < 1 || !finiteEpoch(row.createdAtTs) || !finiteEpoch(row.updatedAtTs) || row.updatedAtTs < row.createdAtTs || row.lastHeartbeatAtTs !== undefined && !finiteEpoch(row.lastHeartbeatAtTs) || row.deadlineAtTs !== undefined && !finiteEpoch(row.deadlineAtTs) || row.leaseOwner !== undefined && !atom(row.leaseOwner) || !Array.isArray(row.events) || row.events.length < 1 || row.events.length > OPERATION_MAX_EVENTS) {
|
|
53
|
+
throw new Error("Operation snapshot coordinates are invalid.");
|
|
54
|
+
}
|
|
55
|
+
const events = row.events.map((event, index) => {
|
|
56
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
57
|
+
throw new Error("Operation event is invalid.");
|
|
58
|
+
const value = event;
|
|
59
|
+
if (value.sequence !== index + 1 || !atom(value.stage) || !OPERATION_STATUSES.includes(value.status) || !finiteEpoch(value.atTs) || !Number.isSafeInteger(value.attempt) || value.attempt < 1 || !["platform-api", "bootstrap-runner", "metal-agent", "compute-agent", "worker"].includes(String(value.origin)) || value.nodeKey !== undefined && !atom(value.nodeKey))
|
|
60
|
+
throw new Error("Operation event is invalid.");
|
|
61
|
+
return { ...value, ...value.failure ? { failure: validateOperationFailure(value.failure) } : {} };
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
...row,
|
|
65
|
+
childOperationKeys: [...row.childOperationKeys],
|
|
66
|
+
...row.failure ? { failure: validateOperationFailure(row.failure) } : {},
|
|
67
|
+
events
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function validateOperationLogBatch(input) {
|
|
71
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
72
|
+
throw new Error("Operation log batch must be an object.");
|
|
73
|
+
const row = input;
|
|
74
|
+
if (!atom(row.operationKey) || !Number.isSafeInteger(row.fromSequence) || !Number.isSafeInteger(row.toSequence) || row.fromSequence < 1 || row.toSequence < row.fromSequence || typeof row.terminal !== "boolean" || !Array.isArray(row.lines) || row.lines.length < 1 || row.lines.length > OPERATION_MAX_LOG_LINES) {
|
|
75
|
+
throw new Error("Operation log batch coordinates are invalid.");
|
|
76
|
+
}
|
|
77
|
+
const lines = row.lines.map((line, index) => {
|
|
78
|
+
if (!line || typeof line !== "object" || Array.isArray(line))
|
|
79
|
+
throw new Error("Operation log line is invalid.");
|
|
80
|
+
const value = line;
|
|
81
|
+
const expected = row.fromSequence + index;
|
|
82
|
+
if (value.sequence !== expected || !finiteEpoch(value.atTs) || !["stdout", "stderr", "system"].includes(String(value.stream)) || typeof value.text !== "string" || !value.text || new TextEncoder().encode(value.text).byteLength > OPERATION_MAX_LOG_TEXT_BYTES) {
|
|
83
|
+
throw new Error("Operation log line is invalid or non-contiguous.");
|
|
84
|
+
}
|
|
85
|
+
return { sequence: value.sequence, atTs: value.atTs, stream: value.stream, text: redactOperationLogText(value.text) };
|
|
86
|
+
});
|
|
87
|
+
if (lines.at(-1).sequence !== row.toSequence)
|
|
88
|
+
throw new Error("Operation log range does not match its lines.");
|
|
89
|
+
return {
|
|
90
|
+
operationKey: row.operationKey,
|
|
91
|
+
fromSequence: row.fromSequence,
|
|
92
|
+
toSequence: row.toSequence,
|
|
93
|
+
terminal: row.terminal,
|
|
94
|
+
lines
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
validateSurfaceSnapshot,
|
|
99
|
+
validateOperationSnapshot,
|
|
100
|
+
validateOperationLogBatch,
|
|
101
|
+
validateOperationFailure,
|
|
102
|
+
redactOperationLogText,
|
|
103
|
+
OPERATION_STATUSES,
|
|
104
|
+
OPERATION_MAX_LOG_TEXT_BYTES,
|
|
105
|
+
OPERATION_MAX_LOG_LINES,
|
|
106
|
+
OPERATION_MAX_EVENTS
|
|
107
|
+
};
|
package/dist/realtime.d.ts
CHANGED
|
@@ -1,13 +1,80 @@
|
|
|
1
1
|
export declare const REALTIME_MAX_EVENTS = 100;
|
|
2
2
|
export declare const REALTIME_MAX_EVENT_BYTES: number;
|
|
3
3
|
export declare const REALTIME_MAX_BATCH_BYTES: number;
|
|
4
|
+
export declare const REALTIME_FLUSH_BATCH_BYTES: number;
|
|
4
5
|
export declare const REALTIME_MAX_SOCKETS_PER_SHARD = 1000;
|
|
5
6
|
export declare const REALTIME_MAX_SHARDS_PER_TOPIC = 1024;
|
|
6
|
-
export
|
|
7
|
+
export type RealtimeResourceScope = Readonly<{
|
|
8
|
+
realmId: string;
|
|
9
|
+
projectKey?: string;
|
|
10
|
+
}>;
|
|
11
|
+
export type RealtimeEvent = Readonly<{
|
|
12
|
+
id: string;
|
|
13
|
+
type: 'resource.patch';
|
|
14
|
+
payload: {
|
|
15
|
+
scope: RealtimeResourceScope;
|
|
16
|
+
resourceKind: string;
|
|
17
|
+
resourceKey: string;
|
|
18
|
+
previousRevision?: string;
|
|
19
|
+
revision: string;
|
|
20
|
+
projection: unknown;
|
|
21
|
+
};
|
|
22
|
+
}> | Readonly<{
|
|
23
|
+
id: string;
|
|
24
|
+
type: 'operation.state';
|
|
25
|
+
payload: {
|
|
26
|
+
operationKey: string;
|
|
27
|
+
parentOperationKey?: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
stage: string;
|
|
30
|
+
status: string;
|
|
31
|
+
attempt: number;
|
|
32
|
+
revision: number;
|
|
33
|
+
updatedAtTs: number;
|
|
34
|
+
failure?: {
|
|
35
|
+
code: string;
|
|
36
|
+
detail: string;
|
|
37
|
+
retryable: boolean;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}> | Readonly<{
|
|
41
|
+
id: string;
|
|
42
|
+
type: 'operation.log';
|
|
43
|
+
payload: {
|
|
44
|
+
operationKey: string;
|
|
45
|
+
fromSequence: number;
|
|
46
|
+
toSequence: number;
|
|
47
|
+
terminal: boolean;
|
|
48
|
+
lines: readonly {
|
|
49
|
+
sequence: number;
|
|
50
|
+
atTs: number;
|
|
51
|
+
stream: 'stdout' | 'stderr' | 'system';
|
|
52
|
+
text: string;
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
55
|
+
}> | Readonly<{
|
|
56
|
+
id: string;
|
|
57
|
+
type: 'notification.summary';
|
|
58
|
+
payload: {
|
|
59
|
+
unread: number;
|
|
60
|
+
latest?: {
|
|
61
|
+
key: string;
|
|
62
|
+
title: string;
|
|
63
|
+
message: string;
|
|
64
|
+
tone: 'warning' | 'error' | 'info';
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}> | Readonly<{
|
|
68
|
+
id: string;
|
|
69
|
+
type: 'session.changed';
|
|
70
|
+
payload: {
|
|
71
|
+
revision: string;
|
|
72
|
+
};
|
|
73
|
+
}> | Readonly<{
|
|
7
74
|
id: string;
|
|
8
75
|
type: string;
|
|
9
76
|
payload: unknown;
|
|
10
|
-
}
|
|
77
|
+
}>;
|
|
11
78
|
export type RealtimePrincipalKind = 'user' | 'node' | 'api-key' | 'service' | 'header';
|
|
12
79
|
export type RealtimeAudience = Readonly<{
|
|
13
80
|
kind: 'public';
|
|
@@ -29,7 +96,7 @@ export type RealtimeAudience = Readonly<{
|
|
|
29
96
|
capability: string;
|
|
30
97
|
}>;
|
|
31
98
|
export interface RealtimeBatch {
|
|
32
|
-
version: 2;
|
|
99
|
+
version: 2 | 3;
|
|
33
100
|
batchId: string;
|
|
34
101
|
topic: string;
|
|
35
102
|
audience: RealtimeAudience;
|
|
@@ -40,7 +107,7 @@ export declare function validateRealtimeAudience(input: unknown): RealtimeAudien
|
|
|
40
107
|
export declare function validateRealtimeBatch(input: unknown): RealtimeBatch;
|
|
41
108
|
export declare const realtimeBatchBytes: (input: unknown) => string;
|
|
42
109
|
export interface RealtimeSubscriptionTicket {
|
|
43
|
-
version: 2;
|
|
110
|
+
version: 2 | 3;
|
|
44
111
|
topic: string;
|
|
45
112
|
principal: Readonly<{
|
|
46
113
|
kind: RealtimePrincipalKind;
|
|
@@ -50,9 +117,20 @@ export interface RealtimeSubscriptionTicket {
|
|
|
50
117
|
projectKeys: readonly string[];
|
|
51
118
|
groups: readonly string[];
|
|
52
119
|
capabilities: readonly string[];
|
|
120
|
+
subscriptions?: readonly RealtimeSubscription[];
|
|
53
121
|
expiresAtSec: number;
|
|
54
122
|
nonce: string;
|
|
55
123
|
}
|
|
124
|
+
export type RealtimeSubscription = Readonly<{
|
|
125
|
+
kind: 'notifications';
|
|
126
|
+
}> | Readonly<{
|
|
127
|
+
kind: 'project';
|
|
128
|
+
projectKey: string;
|
|
129
|
+
}> | Readonly<{
|
|
130
|
+
kind: 'operation';
|
|
131
|
+
operationKey: string;
|
|
132
|
+
logs: boolean;
|
|
133
|
+
}>;
|
|
56
134
|
export declare function validateRealtimeSubscriptionTicket(input: unknown, nowSec?: number): RealtimeSubscriptionTicket;
|
|
57
135
|
/**
|
|
58
136
|
* The edge has no database authority. It may deliver only when the short-lived,
|
|
@@ -61,7 +139,11 @@ export declare function validateRealtimeSubscriptionTicket(input: unknown, nowSe
|
|
|
61
139
|
* decision.
|
|
62
140
|
*/
|
|
63
141
|
export declare function canReceiveRealtimeAudience(ticket: RealtimeSubscriptionTicket, audience: RealtimeAudience): boolean;
|
|
142
|
+
/** v3 tickets authorize the logical stream in addition to its audience. */
|
|
143
|
+
export declare function canReceiveRealtimeEvent(ticket: RealtimeSubscriptionTicket, event: RealtimeEvent): boolean;
|
|
64
144
|
export declare const realtimeShardKey: (topic: string, shard: number) => string;
|
|
145
|
+
/** Stable principal bucket. No directory read is needed to route a principal event. */
|
|
146
|
+
export declare function realtimePrincipalTopic(realmId: string, principalKey: string, buckets?: number): string;
|
|
65
147
|
export declare function realtimeHmac(secret: string, message: string): Promise<string>;
|
|
66
148
|
export declare function verifyRealtimeHmac(secret: string, message: string, signature: string): Promise<boolean>;
|
|
67
149
|
export declare function issueRealtimeSubscriptionTicket(secret: string, ticket: RealtimeSubscriptionTicket, nowSec?: number): Promise<string>;
|
package/dist/realtime.js
CHANGED
|
@@ -6,15 +6,105 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
// src/operation.ts
|
|
10
|
+
var OPERATION_STATUSES = [
|
|
11
|
+
"queued",
|
|
12
|
+
"running",
|
|
13
|
+
"stalled",
|
|
14
|
+
"failed",
|
|
15
|
+
"succeeded",
|
|
16
|
+
"cancelled",
|
|
17
|
+
"compensating",
|
|
18
|
+
"compensated"
|
|
19
|
+
];
|
|
20
|
+
var ATOM = /^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/;
|
|
21
|
+
var FAILURE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
22
|
+
var OPERATION_MAX_EVENTS = 128;
|
|
23
|
+
var OPERATION_MAX_LOG_LINES = 100;
|
|
24
|
+
var OPERATION_MAX_LOG_TEXT_BYTES = 8 * 1024;
|
|
25
|
+
function redactOperationLogText(input) {
|
|
26
|
+
return input.replace(/-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/gi, "[REDACTED_PRIVATE_KEY]").replace(/\b(?:Bearer\s+)?(?:gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|fze_[A-Za-z0-9_-]{16,})\b/gi, "[REDACTED_TOKEN]").replace(/\b(password|secret|token|api[_-]?key)\s*[:=]\s*([^\s,;]+)/gi, "$1=[REDACTED]").slice(0, OPERATION_MAX_LOG_TEXT_BYTES);
|
|
27
|
+
}
|
|
28
|
+
var finiteEpoch = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
29
|
+
var atom = (value) => typeof value === "string" && ATOM.test(value);
|
|
30
|
+
function validateSurfaceSnapshot(input, validateData) {
|
|
31
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
32
|
+
throw new Error("Surface snapshot must be an object.");
|
|
33
|
+
const row = input;
|
|
34
|
+
if (!atom(row.revision) || !finiteEpoch(row.generatedAtTs) || !validateData(row.data)) {
|
|
35
|
+
throw new Error("Surface snapshot is invalid.");
|
|
36
|
+
}
|
|
37
|
+
return { revision: row.revision, generatedAtTs: row.generatedAtTs, data: row.data };
|
|
38
|
+
}
|
|
39
|
+
function validateOperationFailure(input) {
|
|
40
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
41
|
+
throw new Error("Operation failure must be an object.");
|
|
42
|
+
const row = input;
|
|
43
|
+
if (typeof row.code !== "string" || !FAILURE_CODE.test(row.code) || typeof row.detail !== "string" || !row.detail.trim() || new TextEncoder().encode(row.detail).byteLength > 2000 || typeof row.retryable !== "boolean") {
|
|
44
|
+
throw new Error("Operation failure is invalid.");
|
|
45
|
+
}
|
|
46
|
+
return { code: row.code, detail: row.detail.trim(), retryable: row.retryable };
|
|
47
|
+
}
|
|
48
|
+
function validateOperationSnapshot(input) {
|
|
49
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
50
|
+
throw new Error("Operation snapshot must be an object.");
|
|
51
|
+
const row = input;
|
|
52
|
+
if (!atom(row.operationKey) || !atom(row.kind) || !atom(row.correlationId) || row.parentOperationKey !== undefined && !atom(row.parentOperationKey) || !Array.isArray(row.childOperationKeys) || row.childOperationKeys.length > 128 || row.childOperationKeys.some((key) => !atom(key)) || !atom(row.stage) || !OPERATION_STATUSES.includes(row.status) || !Number.isSafeInteger(row.attempt) || row.attempt < 1 || !Number.isSafeInteger(row.revision) || row.revision < 1 || !finiteEpoch(row.createdAtTs) || !finiteEpoch(row.updatedAtTs) || row.updatedAtTs < row.createdAtTs || row.lastHeartbeatAtTs !== undefined && !finiteEpoch(row.lastHeartbeatAtTs) || row.deadlineAtTs !== undefined && !finiteEpoch(row.deadlineAtTs) || row.leaseOwner !== undefined && !atom(row.leaseOwner) || !Array.isArray(row.events) || row.events.length < 1 || row.events.length > OPERATION_MAX_EVENTS) {
|
|
53
|
+
throw new Error("Operation snapshot coordinates are invalid.");
|
|
54
|
+
}
|
|
55
|
+
const events = row.events.map((event, index) => {
|
|
56
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
57
|
+
throw new Error("Operation event is invalid.");
|
|
58
|
+
const value = event;
|
|
59
|
+
if (value.sequence !== index + 1 || !atom(value.stage) || !OPERATION_STATUSES.includes(value.status) || !finiteEpoch(value.atTs) || !Number.isSafeInteger(value.attempt) || value.attempt < 1 || !["platform-api", "bootstrap-runner", "metal-agent", "compute-agent", "worker"].includes(String(value.origin)) || value.nodeKey !== undefined && !atom(value.nodeKey))
|
|
60
|
+
throw new Error("Operation event is invalid.");
|
|
61
|
+
return { ...value, ...value.failure ? { failure: validateOperationFailure(value.failure) } : {} };
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
...row,
|
|
65
|
+
childOperationKeys: [...row.childOperationKeys],
|
|
66
|
+
...row.failure ? { failure: validateOperationFailure(row.failure) } : {},
|
|
67
|
+
events
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function validateOperationLogBatch(input) {
|
|
71
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
72
|
+
throw new Error("Operation log batch must be an object.");
|
|
73
|
+
const row = input;
|
|
74
|
+
if (!atom(row.operationKey) || !Number.isSafeInteger(row.fromSequence) || !Number.isSafeInteger(row.toSequence) || row.fromSequence < 1 || row.toSequence < row.fromSequence || typeof row.terminal !== "boolean" || !Array.isArray(row.lines) || row.lines.length < 1 || row.lines.length > OPERATION_MAX_LOG_LINES) {
|
|
75
|
+
throw new Error("Operation log batch coordinates are invalid.");
|
|
76
|
+
}
|
|
77
|
+
const lines = row.lines.map((line, index) => {
|
|
78
|
+
if (!line || typeof line !== "object" || Array.isArray(line))
|
|
79
|
+
throw new Error("Operation log line is invalid.");
|
|
80
|
+
const value = line;
|
|
81
|
+
const expected = row.fromSequence + index;
|
|
82
|
+
if (value.sequence !== expected || !finiteEpoch(value.atTs) || !["stdout", "stderr", "system"].includes(String(value.stream)) || typeof value.text !== "string" || !value.text || new TextEncoder().encode(value.text).byteLength > OPERATION_MAX_LOG_TEXT_BYTES) {
|
|
83
|
+
throw new Error("Operation log line is invalid or non-contiguous.");
|
|
84
|
+
}
|
|
85
|
+
return { sequence: value.sequence, atTs: value.atTs, stream: value.stream, text: redactOperationLogText(value.text) };
|
|
86
|
+
});
|
|
87
|
+
if (lines.at(-1).sequence !== row.toSequence)
|
|
88
|
+
throw new Error("Operation log range does not match its lines.");
|
|
89
|
+
return {
|
|
90
|
+
operationKey: row.operationKey,
|
|
91
|
+
fromSequence: row.fromSequence,
|
|
92
|
+
toSequence: row.toSequence,
|
|
93
|
+
terminal: row.terminal,
|
|
94
|
+
lines
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
9
98
|
// src/realtime.ts
|
|
10
99
|
var REALTIME_MAX_EVENTS = 100;
|
|
11
100
|
var REALTIME_MAX_EVENT_BYTES = 64 * 1024;
|
|
12
101
|
var REALTIME_MAX_BATCH_BYTES = 512 * 1024;
|
|
102
|
+
var REALTIME_FLUSH_BATCH_BYTES = 256 * 1024;
|
|
13
103
|
var REALTIME_MAX_SOCKETS_PER_SHARD = 1000;
|
|
14
104
|
var REALTIME_MAX_SHARDS_PER_TOPIC = 1024;
|
|
15
|
-
var
|
|
105
|
+
var ATOM2 = /^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/;
|
|
16
106
|
var PRINCIPAL_KINDS = new Set(["user", "node", "api-key", "service", "header"]);
|
|
17
|
-
var
|
|
107
|
+
var atom2 = (value) => typeof value === "string" && ATOM2.test(value);
|
|
18
108
|
function validateRealtimeAudience(input) {
|
|
19
109
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
20
110
|
throw new Error("Realtime audience must be an object.");
|
|
@@ -24,9 +114,9 @@ function validateRealtimeAudience(input) {
|
|
|
24
114
|
throw new Error("Public realtime audience has no additional coordinates.");
|
|
25
115
|
return { kind: "public" };
|
|
26
116
|
}
|
|
27
|
-
if (!
|
|
117
|
+
if (!atom2(row.realmId) || !atom2(row.capability))
|
|
28
118
|
throw new Error("Realtime audience scope is invalid.");
|
|
29
|
-
if (row.kind === "principal" && PRINCIPAL_KINDS.has(row.principalKind) &&
|
|
119
|
+
if (row.kind === "principal" && PRINCIPAL_KINDS.has(row.principalKind) && atom2(row.principalKey)) {
|
|
30
120
|
return {
|
|
31
121
|
kind: "principal",
|
|
32
122
|
realmId: row.realmId,
|
|
@@ -35,10 +125,10 @@ function validateRealtimeAudience(input) {
|
|
|
35
125
|
capability: row.capability
|
|
36
126
|
};
|
|
37
127
|
}
|
|
38
|
-
if (row.kind === "project" &&
|
|
128
|
+
if (row.kind === "project" && atom2(row.projectKey)) {
|
|
39
129
|
return { kind: "project", realmId: row.realmId, projectKey: row.projectKey, capability: row.capability };
|
|
40
130
|
}
|
|
41
|
-
if (row.kind === "group" &&
|
|
131
|
+
if (row.kind === "group" && atom2(row.group)) {
|
|
42
132
|
return { kind: "group", realmId: row.realmId, group: row.group, capability: row.capability };
|
|
43
133
|
}
|
|
44
134
|
throw new Error("Realtime audience coordinates are invalid.");
|
|
@@ -47,25 +137,34 @@ function validateRealtimeBatch(input) {
|
|
|
47
137
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
48
138
|
throw new Error("Realtime batch must be an object.");
|
|
49
139
|
const row = input;
|
|
50
|
-
if (row.version !== 2 || !
|
|
140
|
+
if (row.version !== 2 && row.version !== 3 || !ATOM2.test(row.batchId ?? "") || !ATOM2.test(row.topic ?? "") || !Number.isSafeInteger(row.publishedAtMs) || row.publishedAtMs < 0 || !Array.isArray(row.events) || row.events.length < 1 || row.events.length > REALTIME_MAX_EVENTS)
|
|
51
141
|
throw new Error("Realtime batch coordinates are invalid.");
|
|
52
142
|
const audience = validateRealtimeAudience(row.audience);
|
|
53
143
|
const events = row.events.map((event) => {
|
|
54
144
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
55
145
|
throw new Error("Realtime event must be an object.");
|
|
56
146
|
const value = event;
|
|
57
|
-
if (!
|
|
147
|
+
if (!ATOM2.test(value.id ?? "") || !ATOM2.test(value.type ?? ""))
|
|
58
148
|
throw new Error("Realtime event id/type is invalid.");
|
|
59
149
|
const encoded = JSON.stringify(value.payload);
|
|
60
150
|
if (encoded === undefined || new TextEncoder().encode(encoded).byteLength > REALTIME_MAX_EVENT_BYTES) {
|
|
61
151
|
throw new Error("Realtime event payload is too large or not JSON serializable.");
|
|
62
152
|
}
|
|
153
|
+
if (row.version === 3 && value.type === "operation.log")
|
|
154
|
+
validateOperationLogBatch(value.payload);
|
|
155
|
+
if (row.version === 3 && value.type === "resource.patch") {
|
|
156
|
+
const payload = value.payload;
|
|
157
|
+
const scope = payload.scope;
|
|
158
|
+
if (!scope || !atom2(scope.realmId) || scope.projectKey !== undefined && !atom2(scope.projectKey) || !atom2(payload.resourceKind) || !atom2(payload.resourceKey) || !atom2(payload.revision) || payload.previousRevision !== undefined && !atom2(payload.previousRevision)) {
|
|
159
|
+
throw new Error("Realtime resource patch is invalid.");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
63
162
|
return { id: value.id, type: value.type, payload: value.payload };
|
|
64
163
|
});
|
|
65
164
|
if (new Set(events.map(({ id }) => id)).size !== events.length)
|
|
66
165
|
throw new Error("Realtime event ids must be unique in a batch.");
|
|
67
166
|
const batch = {
|
|
68
|
-
version:
|
|
167
|
+
version: row.version,
|
|
69
168
|
batchId: row.batchId,
|
|
70
169
|
topic: row.topic,
|
|
71
170
|
audience,
|
|
@@ -78,8 +177,26 @@ function validateRealtimeBatch(input) {
|
|
|
78
177
|
return batch;
|
|
79
178
|
}
|
|
80
179
|
var realtimeBatchBytes = (input) => JSON.stringify(validateRealtimeBatch(input));
|
|
180
|
+
function validateSubscriptions(value) {
|
|
181
|
+
if (!Array.isArray(value) || value.length > 64)
|
|
182
|
+
throw new Error("Realtime ticket subscriptions are invalid.");
|
|
183
|
+
return value.map((item) => {
|
|
184
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
185
|
+
throw new Error("Realtime subscription is invalid.");
|
|
186
|
+
const row = item;
|
|
187
|
+
if (row.kind === "notifications" && Object.keys(row).length === 1)
|
|
188
|
+
return { kind: "notifications" };
|
|
189
|
+
if (row.kind === "project" && atom2(row.projectKey) && Object.keys(row).length === 2) {
|
|
190
|
+
return { kind: "project", projectKey: row.projectKey };
|
|
191
|
+
}
|
|
192
|
+
if (row.kind === "operation" && atom2(row.operationKey) && typeof row.logs === "boolean" && Object.keys(row).length === 3) {
|
|
193
|
+
return { kind: "operation", operationKey: row.operationKey, logs: row.logs };
|
|
194
|
+
}
|
|
195
|
+
throw new Error("Realtime subscription is invalid.");
|
|
196
|
+
});
|
|
197
|
+
}
|
|
81
198
|
function boundedUniqueAtoms(value, maximum, label) {
|
|
82
|
-
if (!Array.isArray(value) || value.length > maximum || value.some((item) => !
|
|
199
|
+
if (!Array.isArray(value) || value.length > maximum || value.some((item) => !atom2(item)) || new Set(value).size !== value.length)
|
|
83
200
|
throw new Error(`Realtime ticket ${label} are invalid.`);
|
|
84
201
|
return [...value];
|
|
85
202
|
}
|
|
@@ -87,16 +204,17 @@ function validateRealtimeSubscriptionTicket(input, nowSec = Math.floor(Date.now(
|
|
|
87
204
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
88
205
|
throw new Error("Realtime ticket must be an object.");
|
|
89
206
|
const row = input;
|
|
90
|
-
if (row.version !== 2 || !
|
|
207
|
+
if (row.version !== 2 && row.version !== 3 || !atom2(row.topic) || !atom2(row.realmId) || !atom2(row.nonce) || !row.principal || !PRINCIPAL_KINDS.has(row.principal.kind) || !atom2(row.principal.key) || !Number.isSafeInteger(row.expiresAtSec) || row.expiresAtSec <= nowSec || row.expiresAtSec > nowSec + 300 || row.version === 3 && !Array.isArray(row.subscriptions))
|
|
91
208
|
throw new Error("Realtime ticket is invalid or expired.");
|
|
92
209
|
return {
|
|
93
|
-
version:
|
|
210
|
+
version: row.version,
|
|
94
211
|
topic: row.topic,
|
|
95
212
|
principal: { kind: row.principal.kind, key: row.principal.key },
|
|
96
213
|
realmId: row.realmId,
|
|
97
214
|
projectKeys: boundedUniqueAtoms(row.projectKeys, 32, "project keys"),
|
|
98
215
|
groups: boundedUniqueAtoms(row.groups, 16, "groups"),
|
|
99
216
|
capabilities: boundedUniqueAtoms(row.capabilities, 64, "capabilities"),
|
|
217
|
+
...row.version === 3 ? { subscriptions: validateSubscriptions(row.subscriptions) } : {},
|
|
100
218
|
expiresAtSec: row.expiresAtSec,
|
|
101
219
|
nonce: row.nonce
|
|
102
220
|
};
|
|
@@ -113,12 +231,40 @@ function canReceiveRealtimeAudience(ticket, audience) {
|
|
|
113
231
|
return ticket.projectKeys.includes(audience.projectKey);
|
|
114
232
|
return ticket.groups.includes(audience.group);
|
|
115
233
|
}
|
|
234
|
+
function canReceiveRealtimeEvent(ticket, event) {
|
|
235
|
+
if (ticket.version === 2)
|
|
236
|
+
return true;
|
|
237
|
+
const subscriptions = ticket.subscriptions ?? [];
|
|
238
|
+
if (event.type === "notification.summary") {
|
|
239
|
+
return subscriptions.some(({ kind }) => kind === "notifications");
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "operation.state" || event.type === "operation.log") {
|
|
242
|
+
const operationKey = event.payload.operationKey;
|
|
243
|
+
return typeof operationKey === "string" && subscriptions.some((subscription) => subscription.kind === "operation" && subscription.operationKey === operationKey && (event.type !== "operation.log" || subscription.logs));
|
|
244
|
+
}
|
|
245
|
+
if (event.type === "resource.patch") {
|
|
246
|
+
const projectKey = event.payload.scope?.projectKey;
|
|
247
|
+
return typeof projectKey !== "string" || subscriptions.some((subscription) => subscription.kind === "project" && subscription.projectKey === projectKey);
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
116
251
|
var realtimeShardKey = (topic, shard) => {
|
|
117
|
-
if (!
|
|
252
|
+
if (!ATOM2.test(topic) || !Number.isSafeInteger(shard) || shard < 0 || shard >= REALTIME_MAX_SHARDS_PER_TOPIC) {
|
|
118
253
|
throw new Error("Realtime shard coordinates are invalid.");
|
|
119
254
|
}
|
|
120
255
|
return `rt:${topic}:${shard.toString().padStart(4, "0")}`;
|
|
121
256
|
};
|
|
257
|
+
function realtimePrincipalTopic(realmId, principalKey, buckets = 256) {
|
|
258
|
+
if (!atom2(realmId) || !atom2(principalKey) || !Number.isSafeInteger(buckets) || buckets < 1 || buckets > 4096) {
|
|
259
|
+
throw new Error("Realtime principal partition coordinates are invalid.");
|
|
260
|
+
}
|
|
261
|
+
let hash = 2166136261;
|
|
262
|
+
for (const byte of new TextEncoder().encode(`${realmId}\x00${principalKey}`)) {
|
|
263
|
+
hash ^= byte;
|
|
264
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
265
|
+
}
|
|
266
|
+
return `principal:${realmId}:${hash % buckets}`;
|
|
267
|
+
}
|
|
122
268
|
var bytesToHex = (bytes) => [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
123
269
|
var timingEqual = (left, right) => {
|
|
124
270
|
if (left.length !== right.length)
|
|
@@ -172,13 +318,16 @@ export {
|
|
|
172
318
|
validateRealtimeBatch,
|
|
173
319
|
validateRealtimeAudience,
|
|
174
320
|
realtimeShardKey,
|
|
321
|
+
realtimePrincipalTopic,
|
|
175
322
|
realtimeHmac,
|
|
176
323
|
realtimeBatchBytes,
|
|
177
324
|
issueRealtimeSubscriptionTicket,
|
|
325
|
+
canReceiveRealtimeEvent,
|
|
178
326
|
canReceiveRealtimeAudience,
|
|
179
327
|
REALTIME_MAX_SOCKETS_PER_SHARD,
|
|
180
328
|
REALTIME_MAX_SHARDS_PER_TOPIC,
|
|
181
329
|
REALTIME_MAX_EVENT_BYTES,
|
|
182
330
|
REALTIME_MAX_EVENTS,
|
|
183
|
-
REALTIME_MAX_BATCH_BYTES
|
|
331
|
+
REALTIME_MAX_BATCH_BYTES,
|
|
332
|
+
REALTIME_FLUSH_BATCH_BYTES
|
|
184
333
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"types": "./dist/realtime.d.ts",
|
|
52
52
|
"default": "./dist/realtime.js"
|
|
53
53
|
},
|
|
54
|
+
"./operation": {
|
|
55
|
+
"types": "./dist/operation.d.ts",
|
|
56
|
+
"default": "./dist/operation.js"
|
|
57
|
+
},
|
|
54
58
|
"./lifecycle": {
|
|
55
59
|
"types": "./dist/lifecycle.d.ts",
|
|
56
60
|
"default": "./dist/lifecycle.js"
|