@emilia-protocol/gate 0.9.2 → 0.9.4
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 +35 -7
- package/aec-execution.js +301 -0
- package/challenge-store.js +70 -0
- package/enterprise.js +4 -1
- package/ep-assure.mjs +65 -0
- package/evidence.js +273 -4
- package/index.js +125 -23
- package/package.json +20 -2
- package/reliance-kernel.js +114 -0
- package/reports/assurance-package.js +287 -0
- package/store-postgres.js +45 -26
- package/store.js +97 -23
package/store.js
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A receipt authorizes ONE action, once. The gate consumes a receipt's
|
|
6
6
|
* identifier the first time it is used; any later presentation of the same
|
|
7
|
-
* receipt is a replay and is refused. The default store is in-memory;
|
|
8
|
-
*
|
|
7
|
+
* receipt is a replay and is refused. The default store is in-memory; fleets
|
|
8
|
+
* use the ownership-fenced durable contract below.
|
|
9
9
|
*/
|
|
10
10
|
export class MemoryConsumptionStore {
|
|
11
11
|
constructor() {
|
|
12
|
+
this.durable = false;
|
|
13
|
+
this.ownershipFenced = false;
|
|
14
|
+
this.permanentConsumption = false;
|
|
12
15
|
this.seen = new Set();
|
|
13
16
|
this.reserved = new Set();
|
|
14
17
|
}
|
|
@@ -49,41 +52,103 @@ export class MemoryConsumptionStore {
|
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
export const DURABLE_CONSUMPTION_VERSION = 'EP-GATE-DURABLE-CONSUMPTION-v2';
|
|
56
|
+
|
|
57
|
+
const COMMITTED_VALUE = 'committed:v2';
|
|
58
|
+
const RESERVED_PREFIX = 'reserved:v2:';
|
|
59
|
+
|
|
60
|
+
function defaultReservationToken() {
|
|
61
|
+
if (typeof globalThis.crypto?.randomUUID !== 'function') {
|
|
62
|
+
throw new Error('secure crypto.randomUUID() is required for durable reservation fencing');
|
|
63
|
+
}
|
|
64
|
+
return globalThis.crypto.randomUUID();
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
/**
|
|
53
68
|
* Production custody for replay defense: a durable consumption store backed by
|
|
54
69
|
* any shared key-value backend (Redis, Postgres, DynamoDB, ...), so a receipt
|
|
55
70
|
* consumed on one pod/lambda cannot be replayed on another.
|
|
56
71
|
*
|
|
57
|
-
* The backend MUST provide
|
|
58
|
-
*
|
|
72
|
+
* The backend MUST provide atomic insert-if-absent plus atomic conditional
|
|
73
|
+
* transition and delete. Together they make replay defense and reservation
|
|
74
|
+
* ownership sound under concurrency:
|
|
59
75
|
*
|
|
60
76
|
* backend = {
|
|
61
|
-
* async addIfAbsent(key, value): boolean
|
|
62
|
-
*
|
|
63
|
-
* async
|
|
64
|
-
* async delete(key): void
|
|
77
|
+
* async addIfAbsent(key, value): boolean
|
|
78
|
+
* async compareAndSet(key, expected, replacement): boolean
|
|
79
|
+
* async deleteIfValue(key, expected): boolean
|
|
65
80
|
* async has(key): boolean
|
|
66
81
|
* }
|
|
67
82
|
*
|
|
68
|
-
* State per receipt id is a single
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
83
|
+
* State per receipt id is a single ownership-fenced value. A reservation is
|
|
84
|
+
* `reserved:v2:<random token>` and only the store instance holding that token
|
|
85
|
+
* can commit or release it. This prevents a delayed worker from deleting or
|
|
86
|
+
* committing a newer worker's reservation after timeout/failover.
|
|
87
|
+
*
|
|
88
|
+
* Reservations deliberately receive NO TTL. A crash after an external effect
|
|
89
|
+
* has begun is an indeterminate outcome, and automatically reopening the key
|
|
90
|
+
* would permit a duplicate effect. Operators must reconcile abandoned
|
|
91
|
+
* reservations. `ttlSeconds` applies only after a value is committed, when the
|
|
92
|
+
* receipt's own freshness window independently prevents reuse.
|
|
73
93
|
*/
|
|
74
|
-
export function createDurableConsumptionStore(backend, { ttlSeconds } = {}) {
|
|
75
|
-
for (const m of ['addIfAbsent', '
|
|
94
|
+
export function createDurableConsumptionStore(backend, { ttlSeconds, reservationTokenFactory = defaultReservationToken } = {}) {
|
|
95
|
+
for (const m of ['addIfAbsent', 'compareAndSet', 'deleteIfValue', 'has']) {
|
|
76
96
|
if (typeof backend?.[m] !== 'function') {
|
|
77
97
|
throw new Error(`createDurableConsumptionStore: backend must implement async ${m}(). `
|
|
78
|
-
+ 'addIfAbsent MUST be atomic
|
|
98
|
+
+ 'addIfAbsent and conditional transitions MUST be atomic or replay defense is not fleet-safe.');
|
|
79
99
|
}
|
|
80
100
|
}
|
|
101
|
+
if (typeof reservationTokenFactory !== 'function') {
|
|
102
|
+
throw new Error('createDurableConsumptionStore: reservationTokenFactory must be a function');
|
|
103
|
+
}
|
|
104
|
+
if (ttlSeconds !== undefined && ttlSeconds !== null
|
|
105
|
+
&& (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
106
|
+
throw new Error('createDurableConsumptionStore: ttlSeconds must be a positive safe integer when supplied');
|
|
107
|
+
}
|
|
81
108
|
const opt = ttlSeconds ? { ttlSeconds } : undefined;
|
|
109
|
+
const ownedReservations = new Map();
|
|
110
|
+
|
|
111
|
+
function ownedValue(key) {
|
|
112
|
+
const token = ownedReservations.get(key);
|
|
113
|
+
if (!token) {
|
|
114
|
+
throw new Error(`durable consumption transition refused: this store does not own reservation ${key}`);
|
|
115
|
+
}
|
|
116
|
+
return `${RESERVED_PREFIX}${token}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
120
|
+
durable: backend.durable === true,
|
|
121
|
+
ownershipFenced: true,
|
|
122
|
+
permanentConsumption: ttlSeconds === undefined || ttlSeconds === null,
|
|
123
|
+
retentionSeconds: ttlSeconds ?? null,
|
|
124
|
+
async reserve(key) {
|
|
125
|
+
const token = reservationTokenFactory();
|
|
126
|
+
if (typeof token !== 'string' || token.length < 16) {
|
|
127
|
+
throw new Error('reservationTokenFactory must return an unpredictable string of at least 16 characters');
|
|
128
|
+
}
|
|
129
|
+
const inserted = (await backend.addIfAbsent(key, `${RESERVED_PREFIX}${token}`)) === true;
|
|
130
|
+
if (inserted) ownedReservations.set(key, token);
|
|
131
|
+
return inserted;
|
|
132
|
+
},
|
|
133
|
+
async commit(key) {
|
|
134
|
+
const expected = ownedValue(key);
|
|
135
|
+
const changed = await backend.compareAndSet(key, expected, COMMITTED_VALUE, opt);
|
|
136
|
+
if (changed !== true) {
|
|
137
|
+
throw new Error(`durable consumption commit refused: reservation ownership was lost for ${key}`);
|
|
138
|
+
}
|
|
139
|
+
ownedReservations.delete(key);
|
|
140
|
+
return true;
|
|
141
|
+
},
|
|
142
|
+
async release(key) {
|
|
143
|
+
const expected = ownedValue(key);
|
|
144
|
+
const deleted = await backend.deleteIfValue(key, expected);
|
|
145
|
+
ownedReservations.delete(key);
|
|
146
|
+
if (deleted !== true) {
|
|
147
|
+
throw new Error(`durable consumption release refused: reservation ownership was lost for ${key}`);
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
},
|
|
151
|
+
async consume(key) { return (await backend.addIfAbsent(key, COMMITTED_VALUE, opt)) === true; },
|
|
87
152
|
async has(key) { return (await backend.has(key)) === true; },
|
|
88
153
|
};
|
|
89
154
|
}
|
|
@@ -92,12 +157,21 @@ export function createDurableConsumptionStore(backend, { ttlSeconds } = {}) {
|
|
|
92
157
|
export function createMemoryBackend() {
|
|
93
158
|
const map = new Map();
|
|
94
159
|
return {
|
|
160
|
+
durable: false,
|
|
95
161
|
async addIfAbsent(key, value) { if (map.has(key)) return false; map.set(key, value); return true; },
|
|
96
|
-
async
|
|
97
|
-
|
|
162
|
+
async compareAndSet(key, expected, replacement) {
|
|
163
|
+
if (map.get(key) !== expected) return false;
|
|
164
|
+
map.set(key, replacement);
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
167
|
+
async deleteIfValue(key, expected) {
|
|
168
|
+
if (map.get(key) !== expected) return false;
|
|
169
|
+
return map.delete(key);
|
|
170
|
+
},
|
|
98
171
|
async has(key) { return map.has(key); },
|
|
172
|
+
async get(key) { return map.get(key); },
|
|
99
173
|
get size() { return map.size; },
|
|
100
174
|
};
|
|
101
175
|
}
|
|
102
176
|
|
|
103
|
-
export default { MemoryConsumptionStore, createDurableConsumptionStore, createMemoryBackend };
|
|
177
|
+
export default { MemoryConsumptionStore, createDurableConsumptionStore, createMemoryBackend, DURABLE_CONSUMPTION_VERSION };
|