@forgezero/providers 0.1.6 → 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/database.d.ts +8 -0
- package/dist/database.js +34 -6
- package/package.json +1 -1
package/dist/database.d.ts
CHANGED
|
@@ -43,6 +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
|
+
/** 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';
|
|
46
50
|
/**
|
|
47
51
|
* Dynamic membership is accepted only as a signed/attested envelope verified
|
|
48
52
|
* by the host. A Worker/KV node list by itself is discovery data, not routing
|
|
@@ -70,6 +74,8 @@ export interface Query {
|
|
|
70
74
|
* caller assertions; this adapter never guesses AQL semantics from text.
|
|
71
75
|
*/
|
|
72
76
|
retrySafety?: 'read' | 'idempotent' | 'never';
|
|
77
|
+
/** Routing hint valid only with `retrySafety: read`; it never changes write routing. */
|
|
78
|
+
readPreference?: 'balanced' | 'prefer-designated';
|
|
73
79
|
/** Required when `retrySafety` is `idempotent`; retained for audit correlation. */
|
|
74
80
|
idempotencyKey?: string;
|
|
75
81
|
}
|
|
@@ -84,6 +90,8 @@ export interface ArangoCoordinatorMember {
|
|
|
84
90
|
dirtyFollowerReads: false;
|
|
85
91
|
};
|
|
86
92
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
93
|
+
/** Signed operator preference for safe reads; this is not an ArangoDB role. */
|
|
94
|
+
readPreferred?: boolean;
|
|
87
95
|
/** Milliseconds since epoch, covered by the membership signature/attestation. */
|
|
88
96
|
observedAt: number;
|
|
89
97
|
/** Enrolled hybrid identity. The node key is its Ed25519 public half. */
|
package/dist/database.js
CHANGED
|
@@ -187,18 +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 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
|
+
}
|
|
190
194
|
return {
|
|
191
195
|
members: urls.map((url) => ({
|
|
192
196
|
url,
|
|
193
197
|
role: "coordinator",
|
|
194
198
|
serverMode: "default",
|
|
195
199
|
capabilities: { dirtyFollowerReads: false },
|
|
196
|
-
status: "healthy"
|
|
200
|
+
status: "healthy",
|
|
201
|
+
readPreferred: preferred.includes(url)
|
|
197
202
|
})),
|
|
198
|
-
identity: `static:${urls.join(",")}`
|
|
203
|
+
identity: `static:${urls.join(",")}:preferred:${preferred.join(",")}`
|
|
199
204
|
};
|
|
200
205
|
}
|
|
201
|
-
if (config.url || config.urls) {
|
|
206
|
+
if (config.url || config.urls || config.readPreferredUrls) {
|
|
202
207
|
throw new ProviderError("ARANGO_MEMBERSHIP_AMBIGUOUS", "Configure static coordinator URLs or verified membership, not both.");
|
|
203
208
|
}
|
|
204
209
|
if (!config.clusterId || config.membership.clusterId !== config.clusterId) {
|
|
@@ -216,7 +221,7 @@ async function resolveMembership(config, signal) {
|
|
|
216
221
|
throw new ProviderError("ARANGO_MEMBERSHIP_EXPIRED", "Dynamic ArangoDB membership is not currently valid.");
|
|
217
222
|
}
|
|
218
223
|
const validAuthority = snapshot.authority === "forgezero-platform-signed" || snapshot.authority === "node-attested";
|
|
219
|
-
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.role === "coordinator" && node.serverMode === "default" && node.capabilities?.dirtyFollowerReads === false);
|
|
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);
|
|
220
225
|
if (!validAuthority || !snapshot.revision || !validNodes) {
|
|
221
226
|
throw new ProviderError("ARANGO_MEMBERSHIP_INVALID", "Dynamic ArangoDB membership needs a revision and observed timestamps.");
|
|
222
227
|
}
|
|
@@ -255,6 +260,12 @@ function boundedAttempts(config, available, safe) {
|
|
|
255
260
|
return Math.min(requested, available);
|
|
256
261
|
}
|
|
257
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
|
+
}
|
|
258
269
|
if (request.retrySafety === "read")
|
|
259
270
|
return true;
|
|
260
271
|
if (request.retrySafety !== "idempotent")
|
|
@@ -279,7 +290,10 @@ function selectNode(pool, excluded, now, eligibleMember) {
|
|
|
279
290
|
}
|
|
280
291
|
return;
|
|
281
292
|
}
|
|
282
|
-
function nodeForRequest(pool, excluded, now) {
|
|
293
|
+
function nodeForRequest(pool, request, fallback, excluded, now) {
|
|
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));
|
|
296
|
+
}
|
|
283
297
|
return selectNode(pool, excluded, now, () => true);
|
|
284
298
|
}
|
|
285
299
|
var arangodb = defineProvider({
|
|
@@ -318,6 +332,20 @@ var arangodb = defineProvider({
|
|
|
318
332
|
title: "Coordinator URLs",
|
|
319
333
|
description: "Authenticated private coordinators in this one logical cluster."
|
|
320
334
|
},
|
|
335
|
+
readPreferredUrls: {
|
|
336
|
+
type: "array",
|
|
337
|
+
minItems: 1,
|
|
338
|
+
maxItems: MAX_ARANGO_COORDINATORS,
|
|
339
|
+
uniqueItems: true,
|
|
340
|
+
items: { type: "string" },
|
|
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"
|
|
348
|
+
},
|
|
321
349
|
clusterId: {
|
|
322
350
|
type: "string",
|
|
323
351
|
title: "Cluster ID",
|
|
@@ -423,7 +451,7 @@ var arangodb = defineProvider({
|
|
|
423
451
|
throw context.signal.reason ?? new ProviderError("ARANGO_ABORTED", "ArangoDB request was cancelled.");
|
|
424
452
|
}
|
|
425
453
|
const now = (config.now ?? Date.now)();
|
|
426
|
-
const node = nodeForRequest(pool, tried, now);
|
|
454
|
+
const node = nodeForRequest(pool, request, config.readPreferredFallback, tried, now);
|
|
427
455
|
if (!node)
|
|
428
456
|
break;
|
|
429
457
|
tried.add(node.url);
|