@forgezero/providers 0.1.5 → 0.1.7
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/dist/binance.js +1 -1
- package/dist/chain.js +1 -1
- package/dist/database.d.ts +11 -19
- package/dist/database.js +32 -43
- package/dist/email.js +1 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/pool.js +1 -1
- package/dist/storage.js +1 -1
- package/dist/translation.js +1 -1
- package/package.json +1 -1
package/dist/binance.js
CHANGED
package/dist/chain.js
CHANGED
package/dist/database.d.ts
CHANGED
|
@@ -43,11 +43,10 @@ export interface ArangoConfig {
|
|
|
43
43
|
url?: string;
|
|
44
44
|
/** Explicit authenticated private coordinators. Bounded to prevent config abuse. */
|
|
45
45
|
urls?: readonly string[];
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
readOnlyUrls?: readonly string[];
|
|
46
|
+
/** Optional subset of `urls` preferred only by explicitly safe reads. */
|
|
47
|
+
readPreferredUrls?: readonly string[];
|
|
48
|
+
/** Whether a preferred read may fall back to the balanced writable pool. */
|
|
49
|
+
readPreferredFallback?: 'balanced' | 'error';
|
|
51
50
|
/**
|
|
52
51
|
* Dynamic membership is accepted only as a signed/attested envelope verified
|
|
53
52
|
* by the host. A Worker/KV node list by itself is discovery data, not routing
|
|
@@ -65,8 +64,6 @@ export interface ArangoConfig {
|
|
|
65
64
|
maxCoordinatorAttempts?: number;
|
|
66
65
|
/** Local transport-failure quarantine. Primarily injectable for tests. */
|
|
67
66
|
unhealthyForMs?: number;
|
|
68
|
-
/** What an explicit read-only preference does when no read-only coordinator can serve it. */
|
|
69
|
-
readOnlyFallback?: 'authoritative' | 'error';
|
|
70
67
|
now?: () => number;
|
|
71
68
|
}
|
|
72
69
|
export interface Query {
|
|
@@ -77,29 +74,24 @@ export interface Query {
|
|
|
77
74
|
* caller assertions; this adapter never guesses AQL semantics from text.
|
|
78
75
|
*/
|
|
79
76
|
retrySafety?: 'read' | 'idempotent' | 'never';
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
* `readonly`. It is valid only with `retrySafety: 'read'`; ForgeZero does not
|
|
83
|
-
* infer AQL semantics. Community 3.11.14 dirty follower reads are not exposed:
|
|
84
|
-
* that server feature is Enterprise-only.
|
|
85
|
-
*/
|
|
86
|
-
readPreference?: 'authoritative' | 'prefer-readonly';
|
|
77
|
+
/** Routing hint valid only with `retrySafety: read`; it never changes write routing. */
|
|
78
|
+
readPreference?: 'balanced' | 'prefer-designated';
|
|
87
79
|
/** Required when `retrySafety` is `idempotent`; retained for audit correlation. */
|
|
88
80
|
idempotencyKey?: string;
|
|
89
81
|
}
|
|
90
82
|
export interface ArangoCoordinatorMember {
|
|
91
83
|
url: string;
|
|
92
|
-
/**
|
|
84
|
+
/** Clients address Coordinators; Community 3.11.14 has no read-only joiner role. */
|
|
93
85
|
role: 'coordinator';
|
|
94
86
|
/** Actual `/_admin/server/mode` state for this Coordinator. */
|
|
95
|
-
serverMode: 'default'
|
|
96
|
-
/** ForgeZero request-routing capability, required to agree with `serverMode`. */
|
|
97
|
-
access: 'read-write' | 'read-only';
|
|
87
|
+
serverMode: 'default';
|
|
98
88
|
capabilities: {
|
|
99
89
|
/** Always false for the exact Community 3.11.14 contract. */
|
|
100
90
|
dirtyFollowerReads: false;
|
|
101
91
|
};
|
|
102
92
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
93
|
+
/** Signed operator preference for safe reads; this is not an ArangoDB role. */
|
|
94
|
+
readPreferred?: boolean;
|
|
103
95
|
/** Milliseconds since epoch, covered by the membership signature/attestation. */
|
|
104
96
|
observedAt: number;
|
|
105
97
|
/** Enrolled hybrid identity. The node key is its Ed25519 public half. */
|
|
@@ -122,7 +114,7 @@ export interface ArangoMembershipSnapshot {
|
|
|
122
114
|
export type ArangoMembershipVerifier = (snapshot: ArangoMembershipSnapshot, signal?: AbortSignal) => Promise<boolean>;
|
|
123
115
|
export declare const MAX_ARANGO_COORDINATORS = 16;
|
|
124
116
|
export declare const ARANGO_COMMUNITY_VERSION: "3.11.14";
|
|
125
|
-
/** Protected
|
|
117
|
+
/** Protected API used to attest cluster-wide `default`/maintenance mode. */
|
|
126
118
|
export declare const ARANGO_SERVER_MODE_PATH: "/_admin/server/mode";
|
|
127
119
|
/**
|
|
128
120
|
* Accept only an explicit, bounded set of private authenticated endpoints.
|
package/dist/database.js
CHANGED
|
@@ -144,7 +144,7 @@ function createRegistry(options) {
|
|
|
144
144
|
}
|
|
145
145
|
return { call };
|
|
146
146
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
147
|
+
var VERSION = "0.1.6";
|
|
148
148
|
|
|
149
149
|
// src/database.ts
|
|
150
150
|
var pools = new Map;
|
|
@@ -187,30 +187,23 @@ function arangoCoordinatorUrls(config) {
|
|
|
187
187
|
async function resolveMembership(config, signal) {
|
|
188
188
|
if (!config.membership) {
|
|
189
189
|
const urls = arangoCoordinatorUrls(config);
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
throw new ProviderError("
|
|
190
|
+
const preferred = config.readPreferredUrls?.length ? arangoCoordinatorUrls({ urls: config.readPreferredUrls }) : [];
|
|
191
|
+
if (preferred.some((url) => !urls.includes(url))) {
|
|
192
|
+
throw new ProviderError("ARANGO_READ_PREFERENCE_INVALID", "Read-preferred coordinators must be a subset of the verified writable coordinator list.");
|
|
193
193
|
}
|
|
194
194
|
return {
|
|
195
|
-
members:
|
|
195
|
+
members: urls.map((url) => ({
|
|
196
196
|
url,
|
|
197
197
|
role: "coordinator",
|
|
198
198
|
serverMode: "default",
|
|
199
|
-
access: "read-write",
|
|
200
199
|
capabilities: { dirtyFollowerReads: false },
|
|
201
|
-
status: "healthy"
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
serverMode: "readonly",
|
|
206
|
-
access: "read-only",
|
|
207
|
-
capabilities: { dirtyFollowerReads: false },
|
|
208
|
-
status: "healthy"
|
|
209
|
-
}))],
|
|
210
|
-
identity: `static:${urls.join(",")}:readonly:${readOnlyUrls.join(",")}`
|
|
200
|
+
status: "healthy",
|
|
201
|
+
readPreferred: preferred.includes(url)
|
|
202
|
+
})),
|
|
203
|
+
identity: `static:${urls.join(",")}:preferred:${preferred.join(",")}`
|
|
211
204
|
};
|
|
212
205
|
}
|
|
213
|
-
if (config.url || config.urls || config.
|
|
206
|
+
if (config.url || config.urls || config.readPreferredUrls) {
|
|
214
207
|
throw new ProviderError("ARANGO_MEMBERSHIP_AMBIGUOUS", "Configure static coordinator URLs or verified membership, not both.");
|
|
215
208
|
}
|
|
216
209
|
if (!config.clusterId || config.membership.clusterId !== config.clusterId) {
|
|
@@ -228,7 +221,7 @@ async function resolveMembership(config, signal) {
|
|
|
228
221
|
throw new ProviderError("ARANGO_MEMBERSHIP_EXPIRED", "Dynamic ArangoDB membership is not currently valid.");
|
|
229
222
|
}
|
|
230
223
|
const validAuthority = snapshot.authority === "forgezero-platform-signed" || snapshot.authority === "node-attested";
|
|
231
|
-
const validNodes = snapshot.coordinators.every((node) => Number.isFinite(node.observedAt) && node.nodeKey.length > 0 && node.nodeKey === node.publicKeys?.ed25519 && Boolean(node.publicKeys.mlDsa) && (node.status === "healthy" || node.status === "degraded" || node.status === "unhealthy") && node.
|
|
224
|
+
const validNodes = snapshot.coordinators.every((node) => Number.isFinite(node.observedAt) && node.nodeKey.length > 0 && node.nodeKey === node.publicKeys?.ed25519 && Boolean(node.publicKeys.mlDsa) && (node.status === "healthy" || node.status === "degraded" || node.status === "unhealthy") && (node.readPreferred === undefined || typeof node.readPreferred === "boolean") && node.role === "coordinator" && node.serverMode === "default" && node.capabilities?.dirtyFollowerReads === false);
|
|
232
225
|
if (!validAuthority || !snapshot.revision || !validNodes) {
|
|
233
226
|
throw new ProviderError("ARANGO_MEMBERSHIP_INVALID", "Dynamic ArangoDB membership needs a revision and observed timestamps.");
|
|
234
227
|
}
|
|
@@ -267,6 +260,12 @@ function boundedAttempts(config, available, safe) {
|
|
|
267
260
|
return Math.min(requested, available);
|
|
268
261
|
}
|
|
269
262
|
function requestIsSafe(request) {
|
|
263
|
+
if (request.readPreference !== undefined && request.readPreference !== "balanced" && request.readPreference !== "prefer-designated") {
|
|
264
|
+
throw new ProviderError("ARANGO_READ_PREFERENCE_INVALID", "Unknown ArangoDB read preference.");
|
|
265
|
+
}
|
|
266
|
+
if (request.readPreference && request.retrySafety !== "read") {
|
|
267
|
+
throw new ProviderError("ARANGO_READ_PREFERENCE_UNSAFE", "ArangoDB read preference requires an explicit read retry-safety assertion.");
|
|
268
|
+
}
|
|
270
269
|
if (request.retrySafety === "read")
|
|
271
270
|
return true;
|
|
272
271
|
if (request.retrySafety !== "idempotent")
|
|
@@ -291,20 +290,11 @@ function selectNode(pool, excluded, now, eligibleMember) {
|
|
|
291
290
|
}
|
|
292
291
|
return;
|
|
293
292
|
}
|
|
294
|
-
function assertReadPolicy(request) {
|
|
295
|
-
if (request.readPreference !== undefined && request.readPreference !== "authoritative" && request.readPreference !== "prefer-readonly") {
|
|
296
|
-
throw new ProviderError("ARANGO_READ_PREFERENCE_INVALID", "Unknown ArangoDB read preference.");
|
|
297
|
-
}
|
|
298
|
-
if (request.readPreference && request.retrySafety !== "read") {
|
|
299
|
-
throw new ProviderError("ARANGO_READ_PREFERENCE_UNSAFE", "ArangoDB read preference requires an explicit read retry-safety assertion.");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
293
|
function nodeForRequest(pool, request, fallback, excluded, now) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return selectNode(pool, excluded, now, (node) => node.member.access === "read-only") ?? (fallback === "error" ? undefined : selectNode(pool, excluded, now, writable));
|
|
294
|
+
if (request.retrySafety === "read" && request.readPreference === "prefer-designated") {
|
|
295
|
+
return selectNode(pool, excluded, now, (node) => node.member.readPreferred === true) ?? (fallback === "error" ? undefined : selectNode(pool, excluded, now, () => true));
|
|
306
296
|
}
|
|
307
|
-
return selectNode(pool, excluded, now,
|
|
297
|
+
return selectNode(pool, excluded, now, () => true);
|
|
308
298
|
}
|
|
309
299
|
var arangodb = defineProvider({
|
|
310
300
|
id: "arangodb",
|
|
@@ -342,14 +332,19 @@ var arangodb = defineProvider({
|
|
|
342
332
|
title: "Coordinator URLs",
|
|
343
333
|
description: "Authenticated private coordinators in this one logical cluster."
|
|
344
334
|
},
|
|
345
|
-
|
|
335
|
+
readPreferredUrls: {
|
|
346
336
|
type: "array",
|
|
347
337
|
minItems: 1,
|
|
348
|
-
maxItems: MAX_ARANGO_COORDINATORS
|
|
338
|
+
maxItems: MAX_ARANGO_COORDINATORS,
|
|
349
339
|
uniqueItems: true,
|
|
350
340
|
items: { type: "string" },
|
|
351
|
-
title: "Read-
|
|
352
|
-
description: "Optional
|
|
341
|
+
title: "Read-preferred Coordinator URLs",
|
|
342
|
+
description: "Optional verified coordinator subset preferred by explicitly safe reads."
|
|
343
|
+
},
|
|
344
|
+
readPreferredFallback: {
|
|
345
|
+
type: "string",
|
|
346
|
+
enum: ["balanced", "error"],
|
|
347
|
+
title: "Read-preferred fallback"
|
|
353
348
|
},
|
|
354
349
|
clusterId: {
|
|
355
350
|
type: "string",
|
|
@@ -369,11 +364,6 @@ var arangodb = defineProvider({
|
|
|
369
364
|
minimum: 1000,
|
|
370
365
|
maximum: 300000,
|
|
371
366
|
title: "Local coordinator quarantine (ms)"
|
|
372
|
-
},
|
|
373
|
-
readOnlyFallback: {
|
|
374
|
-
type: "string",
|
|
375
|
-
enum: ["authoritative", "error"],
|
|
376
|
-
title: "Read-only coordinator fallback"
|
|
377
367
|
}
|
|
378
368
|
}
|
|
379
369
|
},
|
|
@@ -385,11 +375,10 @@ var arangodb = defineProvider({
|
|
|
385
375
|
const membership = await resolveMembership(config, context.signal);
|
|
386
376
|
const members = membership.members;
|
|
387
377
|
const urls = members.map(({ url }) => url);
|
|
388
|
-
assertReadPolicy(request);
|
|
389
378
|
const safe = requestIsSafe(request);
|
|
390
|
-
const available =
|
|
379
|
+
const available = members.length;
|
|
391
380
|
if (available === 0) {
|
|
392
|
-
throw new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE",
|
|
381
|
+
throw new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", "No verified authoritative ArangoDB coordinator is available.");
|
|
393
382
|
}
|
|
394
383
|
const attempts = boundedAttempts(config, available, safe);
|
|
395
384
|
const key = `${membership.identity}/${config.database}/${config.username}`;
|
|
@@ -462,7 +451,7 @@ var arangodb = defineProvider({
|
|
|
462
451
|
throw context.signal.reason ?? new ProviderError("ARANGO_ABORTED", "ArangoDB request was cancelled.");
|
|
463
452
|
}
|
|
464
453
|
const now = (config.now ?? Date.now)();
|
|
465
|
-
const node = nodeForRequest(pool, request, config.
|
|
454
|
+
const node = nodeForRequest(pool, request, config.readPreferredFallback, tried, now);
|
|
466
455
|
if (!node)
|
|
467
456
|
break;
|
|
468
457
|
tried.add(node.url);
|
package/dist/email.js
CHANGED
package/dist/http.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/pool.js
CHANGED
package/dist/storage.js
CHANGED
package/dist/translation.js
CHANGED