@forgezero/providers 0.1.3 → 0.1.5

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 CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/http.ts
150
150
  class BudgetExhausted extends ProviderError {
package/dist/chain.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/chain.ts
150
150
  var hexToNumber = (value) => Number(BigInt(value));
@@ -13,36 +13,124 @@
13
13
  * HEALTH three failed connections mark it degraded, and the same admin
14
14
  * screen that shows email relays shows the database
15
15
  *
16
- * `failover: false` is therefore the default and a deliberate one. Set it true
17
- * only for read replicas, where answering from a stale replica beats not
18
- * answering.
16
+ * Coordinator routing below remains inside one authenticated cluster. It never
17
+ * treats a different database or replica as an automatic write destination.
19
18
  */
20
19
  /** Just enough of an ArangoDB client that this package imports nothing. */
21
20
  export interface ArangoLike {
22
- query<T = unknown>(query: unknown, bindVars?: Record<string, unknown>): Promise<{
21
+ query<T = unknown>(query: unknown, bindVars?: Record<string, unknown>, options?: {
22
+ allowDirtyRead?: boolean;
23
+ }): Promise<{
23
24
  all(): Promise<T[]>;
24
25
  }>;
25
26
  version?(): Promise<{
26
27
  version: string;
27
28
  }>;
29
+ close?(): void | Promise<void>;
28
30
  }
29
- export interface ArangoConfig {
31
+ export interface ArangoConnectOptions {
32
+ /** One coordinator from the verified list; the adapter owns selection. */
30
33
  url: string;
31
34
  database: string;
32
35
  username: string;
36
+ password: string;
37
+ loadBalancingStrategy: 'NONE';
38
+ /** Always zero: only this adapter may decide whether replay is safe. */
39
+ maxRetries: number;
40
+ }
41
+ export interface ArangoConfig {
42
+ /** Backwards-compatible single-coordinator coordinate. */
43
+ url?: string;
44
+ /** Explicit authenticated private coordinators. Bounded to prevent config abuse. */
45
+ urls?: readonly string[];
46
+ /**
47
+ * Optional bootstrap Coordinators already proven to be in Arango `readonly`
48
+ * server mode. Prefer signed, probed membership for production.
49
+ */
50
+ readOnlyUrls?: readonly string[];
51
+ /**
52
+ * Dynamic membership is accepted only as a signed/attested envelope verified
53
+ * by the host. A Worker/KV node list by itself is discovery data, not routing
54
+ * authority.
55
+ */
56
+ membership?: ArangoMembershipSnapshot;
57
+ verifyMembership?: ArangoMembershipVerifier;
58
+ /** Required with dynamic membership, preventing a valid list crossing clusters. */
59
+ clusterId?: string;
60
+ database: string;
61
+ username: string;
33
62
  /** Built by the host, because a connection pool must outlive one call. */
34
- connect?: (options: {
35
- url: string;
36
- database: string;
37
- username: string;
38
- password: string;
39
- }) => ArangoLike;
40
- failover?: boolean;
63
+ connect?: (options: ArangoConnectOptions) => ArangoLike;
64
+ /** Maximum coordinator attempts for an explicitly safe request. */
65
+ maxCoordinatorAttempts?: number;
66
+ /** Local transport-failure quarantine. Primarily injectable for tests. */
67
+ unhealthyForMs?: number;
68
+ /** What an explicit read-only preference does when no read-only coordinator can serve it. */
69
+ readOnlyFallback?: 'authoritative' | 'error';
70
+ now?: () => number;
41
71
  }
42
72
  export interface Query {
43
73
  query: unknown;
44
74
  bindVars?: Record<string, unknown>;
75
+ /**
76
+ * No retry is the secure default. `read` and `idempotent` are explicit
77
+ * caller assertions; this adapter never guesses AQL semantics from text.
78
+ */
79
+ retrySafety?: 'read' | 'idempotent' | 'never';
80
+ /**
81
+ * `prefer-readonly` targets Coordinators whose actual Arango server mode is
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';
87
+ /** Required when `retrySafety` is `idempotent`; retained for audit correlation. */
88
+ idempotencyKey?: string;
45
89
  }
90
+ export interface ArangoCoordinatorMember {
91
+ url: string;
92
+ /** ArangoDB has no read-only DBServer/client role. Clients address Coordinators. */
93
+ role: 'coordinator';
94
+ /** Actual `/_admin/server/mode` state for this Coordinator. */
95
+ serverMode: 'default' | 'readonly';
96
+ /** ForgeZero request-routing capability, required to agree with `serverMode`. */
97
+ access: 'read-write' | 'read-only';
98
+ capabilities: {
99
+ /** Always false for the exact Community 3.11.14 contract. */
100
+ dirtyFollowerReads: false;
101
+ };
102
+ status: 'healthy' | 'degraded' | 'unhealthy';
103
+ /** Milliseconds since epoch, covered by the membership signature/attestation. */
104
+ observedAt: number;
105
+ /** Enrolled hybrid identity. The node key is its Ed25519 public half. */
106
+ nodeKey: string;
107
+ publicKeys: {
108
+ ed25519: string;
109
+ mlDsa: string;
110
+ };
111
+ }
112
+ export interface ArangoMembershipSnapshot {
113
+ authority: 'forgezero-platform-signed' | 'node-attested';
114
+ edition: 'community';
115
+ version: '3.11.14';
116
+ clusterId: string;
117
+ revision: string;
118
+ issuedAt: number;
119
+ expiresAt: number;
120
+ coordinators: readonly ArangoCoordinatorMember[];
121
+ }
122
+ export type ArangoMembershipVerifier = (snapshot: ArangoMembershipSnapshot, signal?: AbortSignal) => Promise<boolean>;
123
+ export declare const MAX_ARANGO_COORDINATORS = 16;
124
+ export declare const ARANGO_COMMUNITY_VERSION: "3.11.14";
125
+ /** Protected per-server API used to attest/set `default` or `readonly` mode. */
126
+ export declare const ARANGO_SERVER_MODE_PATH: "/_admin/server/mode";
127
+ /**
128
+ * Accept only an explicit, bounded set of private authenticated endpoints.
129
+ * Service discovery may propose a new list, but it must pass this authority
130
+ * boundary before a fresh pool is created; public Worker/KV state is never a
131
+ * database routing authority.
132
+ */
133
+ export declare function arangoCoordinatorUrls(config: Pick<ArangoConfig, 'url' | 'urls'>): readonly string[];
46
134
  export declare const arangodb: import("./index").ProviderSpec<Query, unknown[]>;
47
135
  /** Drop memoised connections. Tests, and after a credential rotation. */
48
136
  export declare function resetPools(): void;
package/dist/database.js CHANGED
@@ -144,15 +144,172 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/database.ts
150
150
  var pools = new Map;
151
+ var MAX_ARANGO_COORDINATORS = 16;
152
+ var ARANGO_COMMUNITY_VERSION = "3.11.14";
153
+ var ARANGO_SERVER_MODE_PATH = "/_admin/server/mode";
154
+ function arangoCoordinatorUrls(config) {
155
+ const input = config.urls ?? (config.url ? [config.url] : []);
156
+ if (input.length < 1 || input.length > MAX_ARANGO_COORDINATORS) {
157
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", `ArangoDB needs from 1 to ${MAX_ARANGO_COORDINATORS} private coordinator URLs.`);
158
+ }
159
+ const normalized = input.map((raw) => {
160
+ let value;
161
+ try {
162
+ value = new URL(raw);
163
+ } catch {
164
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinator URL is malformed.");
165
+ }
166
+ if (value.protocol !== "http:" && value.protocol !== "https:") {
167
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinators must use HTTP or HTTPS.");
168
+ }
169
+ if (value.username || value.password || value.search || value.hash || value.pathname && value.pathname !== "/") {
170
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinator URLs contain origins only; credentials and paths are separate.");
171
+ }
172
+ const hostname = value.hostname.replace(/^\[|\]$/g, "").toLowerCase();
173
+ const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)?.slice(1).map(Number);
174
+ const privateV4 = ipv4 && ipv4.every((part) => part >= 0 && part <= 255) && (ipv4[0] === 10 || ipv4[0] === 127 || ipv4[0] === 172 && ipv4[1] >= 16 && ipv4[1] <= 31 || ipv4[0] === 192 && ipv4[1] === 168);
175
+ const privateV6 = hostname === "::1" || /^f[cd][0-9a-f]:/i.test(hostname);
176
+ if (!privateV4 && !privateV6 && hostname !== "localhost") {
177
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinators must be literal private or loopback endpoints.");
178
+ }
179
+ value.hostname = value.hostname.toLowerCase();
180
+ return value.toString().replace(/\/$/, "");
181
+ });
182
+ if (new Set(normalized).size !== normalized.length) {
183
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinator URLs must be unique.");
184
+ }
185
+ return normalized;
186
+ }
187
+ async function resolveMembership(config, signal) {
188
+ if (!config.membership) {
189
+ const urls = arangoCoordinatorUrls(config);
190
+ const readOnlyUrls = config.readOnlyUrls?.length ? arangoCoordinatorUrls({ urls: config.readOnlyUrls }) : [];
191
+ if (urls.length + readOnlyUrls.length > MAX_ARANGO_COORDINATORS || urls.some((url) => readOnlyUrls.includes(url))) {
192
+ throw new ProviderError("ARANGO_COORDINATORS_INVALID", `Writable and read-only ArangoDB coordinator URLs must be disjoint and total at most ${MAX_ARANGO_COORDINATORS}.`);
193
+ }
194
+ return {
195
+ members: [...urls.map((url) => ({
196
+ url,
197
+ role: "coordinator",
198
+ serverMode: "default",
199
+ access: "read-write",
200
+ capabilities: { dirtyFollowerReads: false },
201
+ status: "healthy"
202
+ })), ...readOnlyUrls.map((url) => ({
203
+ url,
204
+ role: "coordinator",
205
+ serverMode: "readonly",
206
+ access: "read-only",
207
+ capabilities: { dirtyFollowerReads: false },
208
+ status: "healthy"
209
+ }))],
210
+ identity: `static:${urls.join(",")}:readonly:${readOnlyUrls.join(",")}`
211
+ };
212
+ }
213
+ if (config.url || config.urls || config.readOnlyUrls) {
214
+ throw new ProviderError("ARANGO_MEMBERSHIP_AMBIGUOUS", "Configure static coordinator URLs or verified membership, not both.");
215
+ }
216
+ if (!config.clusterId || config.membership.clusterId !== config.clusterId) {
217
+ throw new ProviderError("ARANGO_MEMBERSHIP_CLUSTER", "Verified ArangoDB membership does not match the configured cluster.");
218
+ }
219
+ if (!config.verifyMembership || !await config.verifyMembership(config.membership, signal)) {
220
+ throw new ProviderError("ARANGO_MEMBERSHIP_UNVERIFIED", "Dynamic ArangoDB membership must pass the host signature or attestation verifier.");
221
+ }
222
+ const now = (config.now ?? Date.now)();
223
+ const snapshot = config.membership;
224
+ if (snapshot.edition !== "community" || snapshot.version !== ARANGO_COMMUNITY_VERSION) {
225
+ throw new ProviderError("ARANGO_MEMBERSHIP_CONTRACT", `Dynamic ArangoDB membership must describe Community ${ARANGO_COMMUNITY_VERSION}.`);
226
+ }
227
+ if (!Number.isFinite(snapshot.issuedAt) || !Number.isFinite(snapshot.expiresAt) || snapshot.issuedAt > now || snapshot.expiresAt <= now) {
228
+ throw new ProviderError("ARANGO_MEMBERSHIP_EXPIRED", "Dynamic ArangoDB membership is not currently valid.");
229
+ }
230
+ 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.role === "coordinator" && (node.serverMode === "default" || node.serverMode === "readonly") && node.access === (node.serverMode === "readonly" ? "read-only" : "read-write") && node.capabilities?.dirtyFollowerReads === false);
232
+ if (!validAuthority || !snapshot.revision || !validNodes) {
233
+ throw new ProviderError("ARANGO_MEMBERSHIP_INVALID", "Dynamic ArangoDB membership needs a revision and observed timestamps.");
234
+ }
235
+ const allUrls = arangoCoordinatorUrls({ urls: snapshot.coordinators.map((node) => node.url) });
236
+ const members = allUrls.map((url, index) => ({ ...snapshot.coordinators[index], url })).filter((member) => member.status !== "unhealthy");
237
+ if (members.length === 0) {
238
+ throw new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", "Verified membership has no healthy or degraded ArangoDB coordinator.");
239
+ }
240
+ return {
241
+ members,
242
+ identity: `membership:${snapshot.clusterId}`,
243
+ issuedAt: snapshot.issuedAt,
244
+ revision: snapshot.revision
245
+ };
246
+ }
247
+ function retryableCoordinatorFailure(error) {
248
+ const failure = error;
249
+ const status = typeof failure.code === "number" ? failure.code : failure.status;
250
+ if (status === 408 || status === 429 || status === 502 || status === 503 || status === 504)
251
+ return true;
252
+ return typeof failure.code === "string" && [
253
+ "ECONNREFUSED",
254
+ "ECONNRESET",
255
+ "EHOSTUNREACH",
256
+ "ENETUNREACH",
257
+ "ETIMEDOUT"
258
+ ].includes(failure.code);
259
+ }
260
+ function boundedAttempts(config, available, safe) {
261
+ if (!safe)
262
+ return 1;
263
+ const requested = config.maxCoordinatorAttempts ?? available;
264
+ if (!Number.isInteger(requested) || requested < 1 || requested > MAX_ARANGO_COORDINATORS) {
265
+ throw new ProviderError("ARANGO_ATTEMPTS_INVALID", `ArangoDB coordinator attempts must be from 1 to ${MAX_ARANGO_COORDINATORS}.`);
266
+ }
267
+ return Math.min(requested, available);
268
+ }
269
+ function requestIsSafe(request) {
270
+ if (request.retrySafety === "read")
271
+ return true;
272
+ if (request.retrySafety !== "idempotent")
273
+ return false;
274
+ if (!request.idempotencyKey || request.idempotencyKey.length > 128) {
275
+ throw new ProviderError("ARANGO_IDEMPOTENCY_KEY_INVALID", "An idempotent ArangoDB retry needs a non-empty idempotency key of at most 128 characters.");
276
+ }
277
+ return true;
278
+ }
279
+ function selectNode(pool, excluded, now, eligibleMember) {
280
+ const eligible = pool.nodes.filter((node) => !excluded.has(node.url) && node.unhealthyUntil <= now && eligibleMember(node));
281
+ if (eligible.length === 0)
282
+ return;
283
+ const least = Math.min(...eligible.map((node) => node.inFlight));
284
+ for (let offset = 0;offset < pool.nodes.length; offset += 1) {
285
+ const index = (pool.cursor + offset) % pool.nodes.length;
286
+ const node = pool.nodes[index];
287
+ if (!excluded.has(node.url) && node.unhealthyUntil <= now && eligibleMember(node) && node.inFlight === least) {
288
+ pool.cursor = (index + 1) % pool.nodes.length;
289
+ return node;
290
+ }
291
+ }
292
+ return;
293
+ }
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
+ function nodeForRequest(pool, request, fallback, excluded, now) {
303
+ const writable = (node) => node.member.access === "read-write";
304
+ if (request.retrySafety === "read" && request.readPreference === "prefer-readonly") {
305
+ return selectNode(pool, excluded, now, (node) => node.member.access === "read-only") ?? (fallback === "error" ? undefined : selectNode(pool, excluded, now, writable));
306
+ }
307
+ return selectNode(pool, excluded, now, writable);
308
+ }
151
309
  var arangodb = defineProvider({
152
310
  id: "arangodb",
153
311
  service: "database",
154
312
  label: "ArangoDB",
155
- multiInstance: true,
156
313
  credentials: {
157
314
  type: "object",
158
315
  additionalProperties: false,
@@ -164,20 +321,59 @@ var arangodb = defineProvider({
164
321
  config: {
165
322
  type: "object",
166
323
  additionalProperties: false,
167
- required: ["url", "database", "username"],
324
+ required: ["database", "username"],
325
+ anyOf: [
326
+ { required: ["url"] },
327
+ { required: ["urls"] },
328
+ { required: ["clusterId"] }
329
+ ],
168
330
  properties: {
169
331
  url: {
170
332
  type: "string",
171
333
  title: "URL",
172
334
  description: "Bind to loopback. A database reachable from the internet is a database that will be."
173
335
  },
336
+ urls: {
337
+ type: "array",
338
+ minItems: 1,
339
+ maxItems: MAX_ARANGO_COORDINATORS,
340
+ uniqueItems: true,
341
+ items: { type: "string" },
342
+ title: "Coordinator URLs",
343
+ description: "Authenticated private coordinators in this one logical cluster."
344
+ },
345
+ readOnlyUrls: {
346
+ type: "array",
347
+ minItems: 1,
348
+ maxItems: MAX_ARANGO_COORDINATORS - 1,
349
+ uniqueItems: true,
350
+ items: { type: "string" },
351
+ title: "Read-only Coordinator URLs",
352
+ description: "Optional bootstrap endpoints whose protected Arango server mode was probed as readonly."
353
+ },
354
+ clusterId: {
355
+ type: "string",
356
+ title: "Cluster ID",
357
+ description: "Binds host-verified dynamic membership to one logical cluster."
358
+ },
174
359
  database: { type: "string", title: "Database" },
175
360
  username: { type: "string", title: "Username" },
176
- failover: {
177
- type: "boolean",
178
- default: false,
179
- title: "Allow failover",
180
- description: "Off by default. Writing to a different database because the first was slow is data loss with extra steps. Enable only for read replicas."
361
+ maxCoordinatorAttempts: {
362
+ type: "integer",
363
+ minimum: 1,
364
+ maximum: MAX_ARANGO_COORDINATORS,
365
+ title: "Safe request attempts"
366
+ },
367
+ unhealthyForMs: {
368
+ type: "integer",
369
+ minimum: 1000,
370
+ maximum: 300000,
371
+ title: "Local coordinator quarantine (ms)"
372
+ },
373
+ readOnlyFallback: {
374
+ type: "string",
375
+ enum: ["authoritative", "error"],
376
+ title: "Read-only coordinator fallback"
181
377
  }
182
378
  }
183
379
  },
@@ -186,35 +382,112 @@ var arangodb = defineProvider({
186
382
  if (!config.connect) {
187
383
  throw new ProviderError("ARANGO_NO_CONNECT", "Provide a connect function in this provider's config — arangojs on a server; a database is not reachable from an edge runtime.");
188
384
  }
189
- const key = `${config.url}/${config.database}/${config.username}`;
190
- let client = pools.get(key);
191
- if (!client) {
192
- client = config.connect({
193
- url: config.url,
194
- database: config.database,
195
- username: config.username,
196
- password: await context.secret("password")
385
+ const membership = await resolveMembership(config, context.signal);
386
+ const members = membership.members;
387
+ const urls = members.map(({ url }) => url);
388
+ assertReadPolicy(request);
389
+ const safe = requestIsSafe(request);
390
+ const available = request.retrySafety === "read" && request.readPreference === "prefer-readonly" && config.readOnlyFallback === "error" ? members.filter(({ serverMode }) => serverMode === "readonly").length : members.filter(({ serverMode }) => request.retrySafety === "read" && request.readPreference === "prefer-readonly" ? true : serverMode === "default").length;
391
+ if (available === 0) {
392
+ throw new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", request.readPreference === "prefer-readonly" ? "No verified read-only ArangoDB coordinator is available under the configured fallback policy." : "No verified authoritative ArangoDB coordinator is available.");
393
+ }
394
+ const attempts = boundedAttempts(config, available, safe);
395
+ const key = `${membership.identity}/${config.database}/${config.username}`;
396
+ let pool = pools.get(key);
397
+ if (!pool) {
398
+ const password = await context.secret("password");
399
+ pool = {
400
+ nodes: members.map((member) => ({
401
+ url: member.url,
402
+ member,
403
+ client: config.connect({
404
+ url: member.url,
405
+ database: config.database,
406
+ username: config.username,
407
+ password,
408
+ loadBalancingStrategy: "NONE",
409
+ maxRetries: 0
410
+ }),
411
+ inFlight: 0,
412
+ unhealthyUntil: 0
413
+ })),
414
+ cursor: 0,
415
+ membershipIssuedAt: membership.issuedAt,
416
+ membershipRevision: membership.revision
417
+ };
418
+ pools.set(key, pool);
419
+ } else if (membership.issuedAt !== undefined) {
420
+ if (pool.membershipIssuedAt !== undefined && membership.issuedAt < pool.membershipIssuedAt) {
421
+ throw new ProviderError("ARANGO_MEMBERSHIP_ROLLBACK", "ArangoDB membership is older than the last accepted signed revision.");
422
+ }
423
+ const existing = new Map(pool.nodes.map((node) => [node.url, node]));
424
+ const missing = urls.filter((url) => !existing.has(url));
425
+ const password = missing.length > 0 ? await context.secret("password") : undefined;
426
+ const nextNodes = members.map((member) => {
427
+ const retained2 = existing.get(member.url);
428
+ if (retained2) {
429
+ retained2.member = member;
430
+ return retained2;
431
+ }
432
+ return {
433
+ url: member.url,
434
+ member,
435
+ client: config.connect({
436
+ url: member.url,
437
+ database: config.database,
438
+ username: config.username,
439
+ password,
440
+ loadBalancingStrategy: "NONE",
441
+ maxRetries: 0
442
+ }),
443
+ inFlight: 0,
444
+ unhealthyUntil: 0
445
+ };
197
446
  });
198
- pools.set(key, client);
447
+ const retained = new Set(urls);
448
+ for (const removed of pool.nodes.filter((node) => !retained.has(node.url) && node.inFlight === 0)) {
449
+ try {
450
+ await removed.client.close?.();
451
+ } catch {}
452
+ }
453
+ pool.nodes = nextNodes;
454
+ pool.cursor %= nextNodes.length;
455
+ pool.membershipIssuedAt = membership.issuedAt;
456
+ pool.membershipRevision = membership.revision;
457
+ }
458
+ const tried = new Set;
459
+ let lastError;
460
+ for (let attempt = 0;attempt < attempts; attempt += 1) {
461
+ if (context.signal?.aborted) {
462
+ throw context.signal.reason ?? new ProviderError("ARANGO_ABORTED", "ArangoDB request was cancelled.");
463
+ }
464
+ const now = (config.now ?? Date.now)();
465
+ const node = nodeForRequest(pool, request, config.readOnlyFallback, tried, now);
466
+ if (!node)
467
+ break;
468
+ tried.add(node.url);
469
+ node.inFlight += 1;
470
+ try {
471
+ const cursor = await node.client.query(request.query, request.bindVars);
472
+ const result = await cursor.all();
473
+ node.unhealthyUntil = 0;
474
+ return result;
475
+ } catch (error) {
476
+ lastError = error;
477
+ if (!retryableCoordinatorFailure(error))
478
+ throw error;
479
+ const quarantine = Math.min(Math.max(config.unhealthyForMs ?? 15000, 1000), 300000);
480
+ node.unhealthyUntil = now + quarantine;
481
+ if (!safe)
482
+ throw error;
483
+ } finally {
484
+ node.inFlight -= 1;
485
+ }
199
486
  }
200
- const cursor = await client.query(request.query, request.bindVars);
201
- return cursor.all();
487
+ throw lastError ?? new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", "No healthy ArangoDB coordinator is available.");
202
488
  },
203
- classify(error) {
204
- const arango = error;
205
- if (arango.errorNum === 1210)
206
- return "terminal";
207
- if (arango.errorNum === 1202)
208
- return "terminal";
209
- if (arango.errorNum === 1501)
210
- return "terminal";
211
- if (arango.code === 400 || arango.code === 404 || arango.code === 409)
212
- return "terminal";
213
- if (arango.code === 401 || arango.code === 403)
214
- return "retryable";
215
- if (arango.code === 503)
216
- return "backoff";
217
- return "retryable";
489
+ classify() {
490
+ return "terminal";
218
491
  }
219
492
  });
220
493
  function resetPools() {
@@ -224,5 +497,9 @@ var databaseProviders = [arangodb];
224
497
  export {
225
498
  resetPools,
226
499
  databaseProviders,
227
- arangodb
500
+ arangodb,
501
+ arangoCoordinatorUrls,
502
+ MAX_ARANGO_COORDINATORS,
503
+ ARANGO_SERVER_MODE_PATH,
504
+ ARANGO_COMMUNITY_VERSION
228
505
  };
package/dist/email.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/email.ts
150
150
  var recipients = (to) => Array.isArray(to) ? [...to] : [to];
package/dist/http.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/http.ts
150
150
  class BudgetExhausted extends ProviderError {
package/dist/index.d.ts CHANGED
@@ -156,4 +156,4 @@ export interface EmailMessage {
156
156
  from?: string;
157
157
  replyTo?: string;
158
158
  }
159
- export declare const VERSION = "0.1.3";
159
+ export declare const VERSION = "0.1.5";
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
  export {
149
149
  staticConfig,
150
150
  nextHealth,
package/dist/pool.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/http.ts
150
150
  class BudgetExhausted extends ProviderError {
package/dist/storage.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/storage.ts
150
150
  var encoder = new TextEncoder;
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.3";
147
+ var VERSION = "0.1.5";
148
148
 
149
149
  // src/translation.ts
150
150
  var GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
- "//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
3
2
  "name": "@forgezero/providers",
4
- "version": "0.1.3",
3
+ "version": "0.1.5",
5
4
  "type": "module",
6
5
  "publishConfig": {
7
- "access": "public"
6
+ "access": "public",
7
+ "provenance": true
8
8
  },
9
9
  "exports": {
10
10
  ".": {
@@ -71,7 +71,7 @@
71
71
  "repository": {
72
72
  "type": "git",
73
73
  "url": "git+https://github.com/forgezero-net/packages.git",
74
- "directory": "packages/providers"
74
+ "directory": "providers"
75
75
  },
76
76
  "bugs": "https://github.com/forgezero-net/packages/issues",
77
77
  "sideEffects": false,