@opengeni/db 0.23.0 → 0.27.0

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.
Files changed (35) hide show
  1. package/dist/{chunk-3UCHDMKG.js → chunk-EX7CDSGH.js} +196 -2
  2. package/dist/chunk-EX7CDSGH.js.map +1 -0
  3. package/dist/{chunk-L6ADMZHE.js → chunk-IHPCI4GV.js} +5 -1
  4. package/dist/chunk-IHPCI4GV.js.map +1 -0
  5. package/dist/connection-token-resolver.d.ts +24 -2
  6. package/dist/index.d.ts +68 -2
  7. package/dist/index.js +1457 -338
  8. package/dist/index.js.map +1 -1
  9. package/dist/preference-registry.d.ts +14 -0
  10. package/dist/provision-roles.js +1 -1
  11. package/dist/runtime-posture.d.ts +3 -3
  12. package/dist/schema.d.ts +262 -4
  13. package/dist/schema.js +5 -1
  14. package/dist/session-realtime-mirror.d.ts +22 -1
  15. package/dist/workspace-instruction-policies-schema.d.ts +449 -0
  16. package/dist/workspace-instruction-policies.d.ts +88 -8
  17. package/drizzle/0165_document_authority_foundation.sql +259 -0
  18. package/drizzle/0166_connection_disconnect_idempotency.sql +49 -0
  19. package/drizzle/0167_document_index_replay_authority.sql +61 -0
  20. package/drizzle/0168_workspace_instruction_policy_operation_receipts.sql +44 -0
  21. package/drizzle/0169_workspace_instruction_policy_onboarding_proposals.sql +202 -0
  22. package/package.json +3 -3
  23. package/src/connection-token-resolver.ts +79 -16
  24. package/src/index.ts +419 -23
  25. package/src/preference-registry.ts +103 -0
  26. package/src/runtime-posture.ts +4 -0
  27. package/src/schema.ts +86 -0
  28. package/src/session-control.ts +48 -1
  29. package/src/session-queue-commands.ts +45 -11
  30. package/src/session-realtime-mirror.ts +117 -1
  31. package/src/session-realtime.ts +49 -1
  32. package/src/workspace-instruction-policies-schema.ts +116 -0
  33. package/src/workspace-instruction-policies.ts +819 -25
  34. package/dist/chunk-3UCHDMKG.js.map +0 -1
  35. package/dist/chunk-L6ADMZHE.js.map +0 -1
@@ -34,7 +34,14 @@ import {
34
34
  } from "./index";
35
35
 
36
36
  export type ResolveConnectionCredentialResult =
37
- | { status: "ok"; headers: Record<string, string>; connectionId: string; expiresAt?: Date | null }
37
+ | {
38
+ status: "ok";
39
+ headers: Record<string, string>;
40
+ connectionId: string;
41
+ /** Exact durable version when the credential came from the local connection store. */
42
+ connectionVersion?: number;
43
+ expiresAt?: Date | null;
44
+ }
38
45
  | {
39
46
  status: "auth_needed";
40
47
  reason: McpCredentialAuthNeededReason;
@@ -337,6 +344,29 @@ export type ConnectionBrokerDeps = {
337
344
  now: () => Date;
338
345
  };
339
346
 
347
+ export type PermanentConnectionRefreshFailure = {
348
+ workspaceId: string;
349
+ connectionId: string;
350
+ connectionVersion: number;
351
+ subjectId: string | null;
352
+ providerDomain: string;
353
+ httpStatus: number;
354
+ oauthErrorCode: string | null;
355
+ };
356
+
357
+ export type ConnectionTokenResolverOptions = {
358
+ /** Provider-aware transport override used by local/integration adapters. */
359
+ refreshTransport?: RefreshTransportOptions;
360
+ /**
361
+ * Optional provider adapter for an atomic, metadata-aware permanent refresh
362
+ * transition. Returning true means the adapter owned the transition (even if
363
+ * its CAS lost to newer truth); false falls back to generic needs_reauth.
364
+ */
365
+ transitionPermanentRefreshFailure?: (
366
+ failure: PermanentConnectionRefreshFailure,
367
+ ) => Promise<boolean>;
368
+ };
369
+
340
370
  export type RefreshTransportOptions = {
341
371
  fetchImpl?: FetchLike;
342
372
  dnsLookup?: DnsLookup;
@@ -361,6 +391,7 @@ export function buildConnectionTokenResolver(
361
391
  db: Database,
362
392
  settings: Settings,
363
393
  deps: ConnectionBrokerDeps = defaultDeps,
394
+ options: ConnectionTokenResolverOptions = {},
364
395
  ): (input: ResolveConnectionCredentialInput) => Promise<ResolveConnectionCredentialResult> {
365
396
  type CredentialLookupInput = Pick<
366
397
  ResolveConnectionCredentialInput,
@@ -439,6 +470,7 @@ export function buildConnectionTokenResolver(
439
470
  status: "ok",
440
471
  headers,
441
472
  connectionId: cred.id,
473
+ connectionVersion: cred.version,
442
474
  expiresAt: cred.expiresAt,
443
475
  };
444
476
  };
@@ -451,7 +483,7 @@ export function buildConnectionTokenResolver(
451
483
  if (!key) {
452
484
  throw new Error("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured");
453
485
  }
454
- const refreshed = await deps.refresh(cred, ref, settings);
486
+ const refreshed = await deps.refresh(cred, ref, settings, options.refreshTransport);
455
487
  const refreshRecord: Parameters<typeof recordConnectionTokenRefresh>[1] = {
456
488
  id: cred.id,
457
489
  version: cred.version,
@@ -541,19 +573,31 @@ export function buildConnectionTokenResolver(
541
573
  // Only a rejected grant may poison the connection; transient failures
542
574
  // (network errors, AS 5xx) leave it active so the next resolve retries.
543
575
  if (isPermanentRefreshError(error)) {
544
- await deps
545
- .setStatus(
546
- db,
547
- input.workspaceId,
548
- "needs_reauth",
549
- error instanceof Error ? error.message : String(error),
550
- {
576
+ let handled = false;
577
+ if (options.transitionPermanentRefreshFailure) {
578
+ try {
579
+ handled = await options.transitionPermanentRefreshFailure({
580
+ workspaceId: cred.workspaceId,
581
+ connectionId: cred.id,
582
+ connectionVersion: cred.version,
583
+ subjectId: cred.subjectId,
584
+ providerDomain: cred.providerDomain,
585
+ httpStatus: error.httpStatus,
586
+ oauthErrorCode: error.oauthErrorCode,
587
+ });
588
+ } catch {
589
+ handled = false;
590
+ }
591
+ }
592
+ if (!handled) {
593
+ await deps
594
+ .setStatus(db, input.workspaceId, "needs_reauth", error.message, {
551
595
  id: cred.id,
552
596
  version: cred.version,
553
597
  subjectId: cred.subjectId,
554
- },
555
- )
556
- .catch(() => undefined);
598
+ })
599
+ .catch(() => undefined);
600
+ }
557
601
  }
558
602
  return authNeeded(ref, "refresh_failed", cred.id);
559
603
  }
@@ -629,18 +673,20 @@ function canonicalResource(value: string): string {
629
673
 
630
674
  export class ConnectionRefreshHttpError extends Error {
631
675
  readonly httpStatus: number;
676
+ readonly oauthErrorCode: string | null;
632
677
 
633
- constructor(httpStatus: number) {
678
+ constructor(httpStatus: number, oauthErrorCode: string | null = null) {
634
679
  super(`connection refresh failed with HTTP ${httpStatus}`);
635
680
  this.name = "ConnectionRefreshHttpError";
636
681
  this.httpStatus = httpStatus;
682
+ this.oauthErrorCode = oauthErrorCode;
637
683
  }
638
684
  }
639
685
 
640
686
  // The token endpoint rejecting the grant itself means re-auth is the only way
641
687
  // forward. 429 (throttling) and 408 are transient despite being 4xx; network
642
688
  // failures and AS 5xx are likewise retryable.
643
- function isPermanentRefreshError(error: unknown): boolean {
689
+ function isPermanentRefreshError(error: unknown): error is ConnectionRefreshHttpError {
644
690
  return (
645
691
  error instanceof ConnectionRefreshHttpError &&
646
692
  error.httpStatus >= 400 &&
@@ -817,8 +863,10 @@ export async function refreshOAuthConnectionCredential(
817
863
  throw new ConnectionRefreshHttpError(response.status);
818
864
  }
819
865
  if (!response.ok) {
820
- await cancelResponseBody(response);
821
- throw new ConnectionRefreshHttpError(response.status);
866
+ throw new ConnectionRefreshHttpError(
867
+ response.status,
868
+ await readOAuthRefreshErrorCode(response),
869
+ );
822
870
  }
823
871
  const payload = await readResponseJsonBounded<Record<string, unknown>>(
824
872
  response,
@@ -853,6 +901,21 @@ export async function refreshOAuthConnectionCredential(
853
901
  };
854
902
  }
855
903
 
904
+ async function readOAuthRefreshErrorCode(response: Response): Promise<string | null> {
905
+ try {
906
+ const payload = await readResponseJsonBounded<Record<string, unknown>>(
907
+ response,
908
+ OAUTH_MAX_RESPONSE_BYTES,
909
+ "OAuth refresh error response",
910
+ );
911
+ const code = stringValue(payload.error);
912
+ return code && /^[a-z0-9_.-]{1,64}$/i.test(code) ? code : null;
913
+ } catch {
914
+ await cancelResponseBody(response);
915
+ return null;
916
+ }
917
+ }
918
+
856
919
  function expiresAtFromTokenResponse(
857
920
  payload: Record<string, unknown>,
858
921
  fallback: Date | null,