@fabricorg/platform 1.7.0 → 1.9.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 (69) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +250 -3
  3. package/dist/actions/index.cjs +6 -6
  4. package/dist/actions/index.d.cts +9 -2
  5. package/dist/actions/index.d.ts +9 -2
  6. package/dist/actions/index.js +1 -1
  7. package/dist/adapters/index.cjs +37 -9
  8. package/dist/adapters/index.d.cts +421 -1
  9. package/dist/adapters/index.d.ts +421 -1
  10. package/dist/adapters/index.js +1 -1
  11. package/dist/chunk-6HY35TKB.cjs +491 -0
  12. package/dist/chunk-6HY35TKB.cjs.map +1 -0
  13. package/dist/chunk-6UGEIDLC.cjs +26 -0
  14. package/dist/chunk-6UGEIDLC.cjs.map +1 -0
  15. package/dist/{chunk-GU3LZWJD.js → chunk-AFXFAEWA.js} +16 -3
  16. package/dist/chunk-AFXFAEWA.js.map +1 -0
  17. package/dist/chunk-AFXKSIVI.cjs +540 -0
  18. package/dist/chunk-AFXKSIVI.cjs.map +1 -0
  19. package/dist/chunk-BJGZE3GE.js +20 -0
  20. package/dist/chunk-BJGZE3GE.js.map +1 -0
  21. package/dist/{chunk-X3GHYMAJ.cjs → chunk-CRZXTZ36.cjs} +2 -22
  22. package/dist/chunk-CRZXTZ36.cjs.map +1 -0
  23. package/dist/chunk-HEYMMVDP.js +480 -0
  24. package/dist/chunk-HEYMMVDP.js.map +1 -0
  25. package/dist/{chunk-USDLP4E7.cjs → chunk-NNXQZPB3.cjs} +4 -4
  26. package/dist/{chunk-USDLP4E7.cjs.map → chunk-NNXQZPB3.cjs.map} +1 -1
  27. package/dist/{chunk-6QX4I45I.js → chunk-PIDZQAEL.js} +2 -2
  28. package/dist/{chunk-6QX4I45I.js.map → chunk-PIDZQAEL.js.map} +1 -1
  29. package/dist/chunk-Q4Q4JPR3.js +524 -0
  30. package/dist/chunk-Q4Q4JPR3.js.map +1 -0
  31. package/dist/{chunk-ATWWZTRC.js → chunk-TYNIFC7J.js} +3 -18
  32. package/dist/chunk-TYNIFC7J.js.map +1 -0
  33. package/dist/{chunk-XCAOO6KG.cjs → chunk-UGWGHTNC.cjs} +2 -2
  34. package/dist/{chunk-XCAOO6KG.cjs.map → chunk-UGWGHTNC.cjs.map} +1 -1
  35. package/dist/{chunk-45OKKLNY.cjs → chunk-YGIPN3TU.cjs} +17 -2
  36. package/dist/chunk-YGIPN3TU.cjs.map +1 -0
  37. package/dist/{chunk-DCC3UIUJ.js → chunk-ZMQTV5OJ.js} +3 -3
  38. package/dist/{chunk-DCC3UIUJ.js.map → chunk-ZMQTV5OJ.js.map} +1 -1
  39. package/dist/evidence/index.cjs +45 -1
  40. package/dist/evidence/index.d.cts +187 -1
  41. package/dist/evidence/index.d.ts +187 -1
  42. package/dist/evidence/index.js +3 -1
  43. package/dist/governance/index.d.cts +1 -0
  44. package/dist/governance/index.d.ts +1 -0
  45. package/dist/index.cjs +143 -66
  46. package/dist/index.d.cts +3 -3
  47. package/dist/index.d.ts +3 -3
  48. package/dist/index.js +8 -7
  49. package/dist/policies/index.cjs +17 -9
  50. package/dist/policies/index.d.cts +15 -1
  51. package/dist/policies/index.d.ts +15 -1
  52. package/dist/policies/index.js +1 -1
  53. package/dist/privacy/index.cjs +19 -18
  54. package/dist/privacy/index.js +2 -1
  55. package/dist/testing/index.cjs +5 -5
  56. package/dist/testing/index.js +2 -2
  57. package/package.json +1 -1
  58. package/dist/chunk-45OKKLNY.cjs.map +0 -1
  59. package/dist/chunk-ATWWZTRC.js.map +0 -1
  60. package/dist/chunk-FAG2RTPM.js +0 -120
  61. package/dist/chunk-FAG2RTPM.js.map +0 -1
  62. package/dist/chunk-GU3LZWJD.js.map +0 -1
  63. package/dist/chunk-OZLZHISS.js +0 -3
  64. package/dist/chunk-OZLZHISS.js.map +0 -1
  65. package/dist/chunk-PG5LYTUX.cjs +0 -4
  66. package/dist/chunk-PG5LYTUX.cjs.map +0 -1
  67. package/dist/chunk-X3GHYMAJ.cjs.map +0 -1
  68. package/dist/chunk-XZQZQ5SW.cjs +0 -129
  69. package/dist/chunk-XZQZQ5SW.cjs.map +0 -1
@@ -46,5 +46,5 @@ function createActionRegistry(definitions = []) {
46
46
  }
47
47
 
48
48
  export { ACTOR_TYPES, assertActionId, assertActorType, createActionRegistry, isActorType };
49
- //# sourceMappingURL=chunk-6QX4I45I.js.map
50
- //# sourceMappingURL=chunk-6QX4I45I.js.map
49
+ //# sourceMappingURL=chunk-PIDZQAEL.js.map
50
+ //# sourceMappingURL=chunk-PIDZQAEL.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../actions/index.ts"],"names":[],"mappings":";AAyBO,IAAM,WAAA,GAAoC;AAAA,EAChD,gBAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACD;AAEO,SAAS,YAAY,KAAA,EAAmC;AAC9D,EAAA,OAAQ,WAAA,CAAkC,SAAS,KAAK,CAAA;AACzD;AAEO,SAAS,gBAAgB,KAAA,EAA2C;AAC1E,EAAA,IAAI,CAAC,WAAA,CAAY,KAAK,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACD;AAmQO,SAAS,eAAe,QAAA,EAAgD;AAC9E,EAAA,IAAI,CAAC,oCAAA,CAAqC,IAAA,CAAK,QAAQ,CAAA,EAAG;AACzD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,QAAQ,CAAA,CAAE,CAAA;AAAA,EACjD;AACD;AAUO,SAAS,oBAAA,CAAoC,WAAA,GAAgD,EAAC,EAAwB;AAC5H,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAqC;AACzD,EAAA,MAAM,QAAA,GAAW,CAAC,UAAA,KAA4C;AAC7D,IAAA,cAAA,CAAe,WAAW,QAAQ,CAAA;AAClC,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,UAAA,CAAW,QAAQ,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,uBAAA,CAAyB,CAAA;AAC7G,IAAA,IAAI,UAAA,CAAW,aAAA,IAAiB,UAAA,CAAW,WAAA,CAAY,WAAW,CAAA,EAAG;AACpE,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,8FAAA,CAAgG,CAAA;AAAA,IAC/I;AACA,IAAA,IAAI,WAAW,eAAA,KAAoB,MAAA,IAAa,OAAO,UAAA,CAAW,oBAAoB,UAAA,EAAY;AACjG,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,kDAAA,CAAoD,CAAA;AAAA,IACnG;AACA,IAAA,OAAA,CAAQ,GAAA,CAAI,UAAA,CAAW,QAAA,EAAU,UAAU,CAAA;AAAA,EAC5C,CAAA;AACA,EAAA,KAAA,MAAW,UAAA,IAAc,WAAA,EAAa,QAAA,CAAS,UAAU,CAAA;AACzD,EAAA,OAAO;AAAA,IACN,QAAA;AAAA,IACA,kBAAkB,UAAA,EAAY;AAC7B,MAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,WAAW,QAAQ,CAAA,WAAY,UAAU,CAAA;AAAA,IAC3D,CAAA;AAAA,IACA,OAAA,EAAS,CAAC,QAAA,KAAa,OAAA,CAAQ,IAAI,QAAQ,CAAA;AAAA,IAC3C,GAAA,EAAK,CAAC,QAAA,KAAmC,OAAA,CAAQ,IAAI,QAAoB,CAAA;AAAA,IACzE,KAAK,MAAM,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,MAAM;AAAA,GACrC;AACD","file":"chunk-6QX4I45I.js","sourcesContent":["import type { SurfaceDeclaration } from \"../discovery\";\nexport type ActionId = `${string}.${string}`;\n\nexport interface SchemaParseIssue {\n\tpath: PropertyKey[];\n\tmessage: string;\n}\n\nexport type SchemaParseResult<T = unknown> =\n\t| { success: true; data: T }\n\t| { success: false; error: { issues: SchemaParseIssue[] } };\n\nexport interface SchemaValidator<T = unknown> {\n\tparse(input: unknown): T;\n\tsafeParse(input: unknown): SchemaParseResult<T>;\n}\n\nexport type ActorType =\n\t| \"natural_person\"\n\t| \"agent\"\n\t| \"system\"\n\t| \"service_account\"\n\t| \"external_system\"\n\t| \"integration\";\n\nexport const ACTOR_TYPES: readonly ActorType[] = [\n\t\"natural_person\",\n\t\"agent\",\n\t\"system\",\n\t\"service_account\",\n\t\"external_system\",\n\t\"integration\",\n] as const;\n\nexport function isActorType(value: string): value is ActorType {\n\treturn (ACTOR_TYPES as readonly string[]).includes(value);\n}\n\nexport function assertActorType(value: string): asserts value is ActorType {\n\tif (!isActorType(value)) {\n\t\tthrow new Error(`Invalid actorType: ${value}`);\n\t}\n}\n\nexport type ActionStatus =\n\t| \"pending\"\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"blocked_by_policy\"\n\t| \"waiting_for_approval\"\n\t| \"reconciliation_required\"\n\t| \"validation_failed\";\n\nexport interface FabricRuntimeServices {\n\treadonly [serviceName: string]: unknown;\n}\n\nexport interface ActionContext<TDb = unknown> {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tcorrelationId: string;\n\tcausationId?: string;\n\tprovenance?: { source: string; auditAttributes?: Record<string, string> };\n\tdb: TDb;\n\tservices?: FabricRuntimeServices;\n\t/**\n\t * When the host admitted this invocation's submission through the\n\t * action's `admitSubmission` check, read from the durable invocation\n\t * record. Absent when the action declares no check, or when the runtime\n\t * that built this context does not run declared admission checks (a host\n\t * older than the one that introduced them). A handler whose action\n\t * declares `admitSubmission` can refuse to run without it.\n\t */\n\tadmittedAt?: Date;\n}\n\n/**\n * Trusted facts a host gives an action's `admitSubmission` check. Scope and\n * actor come from the host's authenticated submission, never from the\n * parameters.\n */\nexport interface ActionAdmissionInput {\n\treadonly actionId: ActionId;\n\treadonly tenantId: string;\n\treadonly spaceId: string;\n\treadonly actorId: string;\n\treadonly actorType: ActorType;\n\t/**\n\t * The submitted parameters, unvalidated, as a copy made for this check.\n\t * `@fabricorg/platform-host` records its own copy of the same value, taken\n\t * before the check ran, and only if the check admits it; changing this\n\t * copy changes nothing the host records.\n\t */\n\treadonly parameters: Readonly<Record<string, unknown>>;\n\t/**\n\t * Aborted when the host's admission deadline passes. Hosts pass an\n\t * `AbortSignal`. `@fabricorg/platform-host` refuses an answer that settles\n\t * at or after the deadline, even one that settles before the abort.\n\t */\n\treadonly signal: ActionAdmissionSignal;\n}\n\n/**\n * The part of an `AbortSignal` an admission check can rely on. Declared\n * structurally so this module compiles without DOM or Node type libraries; a\n * host passes a real `AbortSignal`, which satisfies it.\n */\nexport interface ActionAdmissionSignal {\n\treadonly aborted: boolean;\n\taddEventListener(type: \"abort\", listener: () => void, options?: { once?: boolean }): void;\n\tremoveEventListener(type: \"abort\", listener: () => void): void;\n}\n\n/**\n * An admission check's answer. Only `{ admitted: true }` admits. A refusal's\n * `code` must be a fixed code chosen by the action (lower-case first letter,\n * then letters, digits, `_`, `.`, `:` or `-`, at most 128 characters), never\n * a submitted value: the host puts it in the error it throws.\n */\nexport type ActionAdmissionDecision =\n\t| { readonly admitted: true }\n\t| { readonly admitted: false; readonly code: string };\n\n/**\n * Decides whether a submission may be recorded at all. A host that supports\n * it runs the check after authorizing the submitter and before it writes\n * anything durable for the submission; a refusal, a throw, a timeout or any\n * answer other than `{ admitted: true }` refuses the submission.\n */\nexport type ActionAdmission = (input: ActionAdmissionInput) => Promise<ActionAdmissionDecision>;\n\nexport interface ActionResult {\n\tsuccess: boolean;\n\tdata?: Record<string, unknown>;\n\terror?: string;\n}\n\nexport type ActionHandler<TDb = unknown> = (\n\tctx: ActionContext<TDb>,\n\tparams: unknown,\n) => Promise<ActionResult>;\n\nexport interface AdapterStepRetryPolicy {\n\tidempotent: boolean;\n\tmaxAttempts?: number;\n\tinitialDelayMs?: number;\n\tbackoffMultiplier?: number;\n\tmaxDelayMs?: number;\n}\n\nexport interface AdapterStep {\n\tadapterType: string;\n\toperation: string;\n\tretryPolicy?: AdapterStepRetryPolicy;\n\tgetInput: (\n\t\tparams: unknown,\n\t\thandlerResult: Record<string, unknown>,\n\t) => Record<string, unknown> | undefined;\n}\n\nexport interface ActionStateMachineBinding {\n\tentityType: string;\n\ttargetState: string | ((params: unknown) => string);\n\tgetEntityId: (params: unknown) => string | undefined;\n}\n\nexport interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {\n\tactionId: ActionId;\n\tnamespace: string;\n\tversion: number;\n\tschema: SchemaValidator;\n\t/**\n\t * Optional check a host runs on every submission before it records\n\t * anything durable for it (invocation, idempotency key, events, errors).\n\t * Use it when parameters must be validated against trusted context, for\n\t * example a secret reference the tenant's secret store must hold, before\n\t * they may be persisted. `@fabricorg/platform-host` runs it from the\n\t * release that introduced it; an older host ignores it, so a handler that\n\t * relies on it should require `ActionContext.admittedAt`.\n\t */\n\tadmitSubmission?: ActionAdmission;\n\t/** Optional validation contract for the public handler result data. */\n\tresultSchema?: SchemaValidator;\n\thandler?: ActionHandler<TDb>;\n\trequiredRoles?: string[];\n\trequiredPermissions?: string[];\n\tpolicies?: `${string}.v${number}`[];\n\temitsEvents: string[];\n\tidempotent: boolean;\n\tmutatesDomain: boolean;\n\t/** Portable execution semantics used by hosts, gateways, and generated capability contracts. */\n\texecution?: ActionExecutionContract;\n\tstateMachine?: ActionStateMachineBinding;\n\tadapterSteps?: AdapterStep[];\n\t/** Completion actions can attest only after all external adapter work succeeds. */\n\teventPhase?: \"before_adapters\" | \"after_adapters\";\n\t/**\n\t * If \"saga\", the action is a multi-step orchestration. The worker dispatches\n\t * to a registered saga implementation instead of running `handler` directly.\n\t */\n\tkind?: \"atomic\" | \"saga\";\n}\n\nexport type ActionConnectivity = \"online-required\" | \"offline-capturable\";\nexport type AuthorityMoment = \"capture\" | \"execution\" | \"both\";\nexport type ActionIdempotencyStance = \"required\" | \"supported\" | \"prohibited\";\nexport type ActionCompletionMode = \"immediate\" | \"accepted\" | \"long-running\";\nexport type ActionConsistency = \"authoritative-now\" | \"provisional-until-reconciled\";\nexport type ActionDataSensitivity = \"public\" | \"internal\" | \"confidential\" | \"restricted\";\n\nexport interface ActionExecutionContract {\n\tconnectivity: ActionConnectivity;\n\tauthorityMoment: AuthorityMoment;\n\tidempotency: ActionIdempotencyStance;\n\t/**\n\t * `immediate`: the invocation settles within the host's execution.\n\t * `accepted` and `long-running`: an adapter may hand the work to an\n\t * external system and the invocation stays running until that system\n\t * completes it by reference through the host's completion ingress.\n\t * Declare `completionDeadlineMs` on a `long-running` action so a callback\n\t * that never comes is noticed rather than waited on forever.\n\t */\n\tcompletion: ActionCompletionMode;\n\t/**\n\t * How long an accepted handoff may wait for its completion before the host\n\t * moves the invocation to reconciliation. Without it, nothing sweeps the\n\t * handoff. Refused on `immediate`, since nothing external completes it.\n\t */\n\tcompletionDeadlineMs?: number;\n\tconsistency: ActionConsistency;\n\tsensitivity?: ActionDataSensitivity;\n}\n\nexport interface SagaContext {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tparameters: unknown;\n\tsleep?: (delayMs: number) => Promise<void>;\n\tquery?: <T = unknown>(model: string, operation: string, args: unknown) => Promise<T>;\n\t/**\n\t * Wait for a named external signal carrying a payload of `T`, with a timeout.\n\t *\n\t * Resolves with the payload of the first signal received, or `null` if the timeout\n\t * fires first. Subsequent signals with the same name received before another\n\t * `awaitSignal` call are buffered (FIFO) so no signal is lost.\n\t *\n\t * Intended for agent-driven and webhook-driven coordination: replace polling loops\n\t * with push-based wakeups while preserving the saga's deterministic replay\n\t * semantics (the runtime is responsible for capturing signals into workflow history).\n\t *\n\t * Optional. Runtimes that cannot implement push-based signaling (e.g. an in-memory\n\t * test harness) may omit this; sagas should treat absence as a fallback condition\n\t * and use `sleep` + `query` polling instead.\n\t *\n\t * Signal *senders* are runtime-specific (e.g. a Temporal `WorkflowClient.signal()`\n\t * call from an action handler). Apps that need to send signals from action\n\t * handlers should expose a coordination service via `ActionContext.services`\n\t * — the platform does not prescribe the sender API.\n\t *\n\t * @example\n\t * const payload = await ctx.awaitSignal?.<{ result: Record<string, unknown> }>(\n\t * \"userInputProcessed\",\n\t * 5 * 60 * 1000,\n\t * );\n\t * if (payload) { /* handle pushed result *\\/ }\n\t * else { /* timeout — fall back to deterministic path *\\/ }\n\t */\n\tawaitSignal?: <T = unknown>(signalName: string, timeoutMs: number) => Promise<T | null>;\n}\n\nexport interface SagaChildActionInput {\n\tsuffix: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n}\n\nexport type SagaRunChild = (input: SagaChildActionInput) => Promise<Record<string, unknown>>;\n\nexport interface SagaCompletionEvent {\n\teventType: string;\n\tsubjectType: string;\n\tsubjectId: string;\n\tpayload: unknown;\n\teventSchemaVersion?: number;\n}\n\nexport interface SagaCompletion {\n\tresultData: Record<string, unknown>;\n\tevent?: SagaCompletionEvent;\n}\n\nexport type SagaImplementation = (\n\tctx: SagaContext,\n\trunChild: SagaRunChild,\n) => Promise<SagaCompletion>;\n\nexport function assertActionId(actionId: string): asserts actionId is ActionId {\n\tif (!/^[a-z][a-z0-9-]*\\.[a-z][a-z0-9_]*$/.test(actionId)) {\n\t\tthrow new Error(`Invalid action ID: ${actionId}`);\n\t}\n}\n\nexport interface ActionRegistry<TDb = unknown> {\n\tregister(definition: ActionDefinition<TDb>): void;\n\tregisterIfMissing(definition: ActionDefinition<TDb>): void;\n\tresolve(actionId: ActionId): ActionDefinition<TDb> | undefined;\n\thas(actionId: string): actionId is ActionId;\n\tids(): ActionId[];\n}\n\nexport function createActionRegistry<TDb = unknown>(definitions: readonly ActionDefinition<TDb>[] = []): ActionRegistry<TDb> {\n\tconst actions = new Map<ActionId, ActionDefinition<TDb>>();\n\tconst register = (definition: ActionDefinition<TDb>): void => {\n\t\tassertActionId(definition.actionId);\n\t\tif (actions.has(definition.actionId)) throw new Error(`Action \"${definition.actionId}\" is already registered`);\n\t\tif (definition.mutatesDomain && definition.emitsEvents.length === 0) {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`);\n\t\t}\n\t\tif (definition.admitSubmission !== undefined && typeof definition.admitSubmission !== \"function\") {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" declares admitSubmission that is not a function.`);\n\t\t}\n\t\tactions.set(definition.actionId, definition);\n\t};\n\tfor (const definition of definitions) register(definition);\n\treturn {\n\t\tregister,\n\t\tregisterIfMissing(definition) {\n\t\t\tif (!actions.has(definition.actionId)) register(definition);\n\t\t},\n\t\tresolve: (actionId) => actions.get(actionId),\n\t\thas: (actionId): actionId is ActionId => actions.has(actionId as ActionId),\n\t\tids: () => Array.from(actions.keys()),\n\t};\n}\n"]}
1
+ {"version":3,"sources":["../actions/index.ts"],"names":[],"mappings":";AAyBO,IAAM,WAAA,GAAoC;AAAA,EAChD,gBAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACD;AAEO,SAAS,YAAY,KAAA,EAAmC;AAC9D,EAAA,OAAQ,WAAA,CAAkC,SAAS,KAAK,CAAA;AACzD;AAEO,SAAS,gBAAgB,KAAA,EAA2C;AAC1E,EAAA,IAAI,CAAC,WAAA,CAAY,KAAK,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACD;AA0QO,SAAS,eAAe,QAAA,EAAgD;AAC9E,EAAA,IAAI,CAAC,oCAAA,CAAqC,IAAA,CAAK,QAAQ,CAAA,EAAG;AACzD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,QAAQ,CAAA,CAAE,CAAA;AAAA,EACjD;AACD;AAUO,SAAS,oBAAA,CAAoC,WAAA,GAAgD,EAAC,EAAwB;AAC5H,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAqC;AACzD,EAAA,MAAM,QAAA,GAAW,CAAC,UAAA,KAA4C;AAC7D,IAAA,cAAA,CAAe,WAAW,QAAQ,CAAA;AAClC,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,UAAA,CAAW,QAAQ,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,uBAAA,CAAyB,CAAA;AAC7G,IAAA,IAAI,UAAA,CAAW,aAAA,IAAiB,UAAA,CAAW,WAAA,CAAY,WAAW,CAAA,EAAG;AACpE,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,8FAAA,CAAgG,CAAA;AAAA,IAC/I;AACA,IAAA,IAAI,WAAW,eAAA,KAAoB,MAAA,IAAa,OAAO,UAAA,CAAW,oBAAoB,UAAA,EAAY;AACjG,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,kDAAA,CAAoD,CAAA;AAAA,IACnG;AACA,IAAA,OAAA,CAAQ,GAAA,CAAI,UAAA,CAAW,QAAA,EAAU,UAAU,CAAA;AAAA,EAC5C,CAAA;AACA,EAAA,KAAA,MAAW,UAAA,IAAc,WAAA,EAAa,QAAA,CAAS,UAAU,CAAA;AACzD,EAAA,OAAO;AAAA,IACN,QAAA;AAAA,IACA,kBAAkB,UAAA,EAAY;AAC7B,MAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,WAAW,QAAQ,CAAA,WAAY,UAAU,CAAA;AAAA,IAC3D,CAAA;AAAA,IACA,OAAA,EAAS,CAAC,QAAA,KAAa,OAAA,CAAQ,IAAI,QAAQ,CAAA;AAAA,IAC3C,GAAA,EAAK,CAAC,QAAA,KAAmC,OAAA,CAAQ,IAAI,QAAoB,CAAA;AAAA,IACzE,KAAK,MAAM,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,MAAM;AAAA,GACrC;AACD","file":"chunk-PIDZQAEL.js","sourcesContent":["import type { SurfaceDeclaration } from \"../discovery\";\nexport type ActionId = `${string}.${string}`;\n\nexport interface SchemaParseIssue {\n\tpath: PropertyKey[];\n\tmessage: string;\n}\n\nexport type SchemaParseResult<T = unknown> =\n\t| { success: true; data: T }\n\t| { success: false; error: { issues: SchemaParseIssue[] } };\n\nexport interface SchemaValidator<T = unknown> {\n\tparse(input: unknown): T;\n\tsafeParse(input: unknown): SchemaParseResult<T>;\n}\n\nexport type ActorType =\n\t| \"natural_person\"\n\t| \"agent\"\n\t| \"system\"\n\t| \"service_account\"\n\t| \"external_system\"\n\t| \"integration\";\n\nexport const ACTOR_TYPES: readonly ActorType[] = [\n\t\"natural_person\",\n\t\"agent\",\n\t\"system\",\n\t\"service_account\",\n\t\"external_system\",\n\t\"integration\",\n] as const;\n\nexport function isActorType(value: string): value is ActorType {\n\treturn (ACTOR_TYPES as readonly string[]).includes(value);\n}\n\nexport function assertActorType(value: string): asserts value is ActorType {\n\tif (!isActorType(value)) {\n\t\tthrow new Error(`Invalid actorType: ${value}`);\n\t}\n}\n\nexport type ActionStatus =\n\t| \"pending\"\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"blocked_by_policy\"\n\t| \"waiting_for_approval\"\n\t| \"reconciliation_required\"\n\t| \"validation_failed\";\n\nexport interface FabricRuntimeServices {\n\treadonly [serviceName: string]: unknown;\n}\n\nexport interface ActionContext<TDb = unknown> {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tcorrelationId: string;\n\tcausationId?: string;\n\tprovenance?: { source: string; auditAttributes?: Record<string, string> };\n\tdb: TDb;\n\tservices?: FabricRuntimeServices;\n\t/**\n\t * When the host admitted this invocation's submission through the\n\t * action's `admitSubmission` check, read from the durable invocation\n\t * record. Absent when the action declares no check, or when the runtime\n\t * that built this context does not run declared admission checks (a host\n\t * older than the one that introduced them). A handler whose action\n\t * declares `admitSubmission` can refuse to run without it.\n\t */\n\tadmittedAt?: Date;\n}\n\n/**\n * Trusted facts a host gives an action's `admitSubmission` check. Scope and\n * actor come from the host's authenticated submission, never from the\n * parameters.\n */\nexport interface ActionAdmissionInput {\n\treadonly actionId: ActionId;\n\treadonly tenantId: string;\n\treadonly spaceId: string;\n\treadonly actorId: string;\n\treadonly actorType: ActorType;\n\t/**\n\t * The submitted parameters, unvalidated, as a copy made for this check.\n\t * `@fabricorg/platform-host` records its own copy of the same value, taken\n\t * before the check ran, and only if the check admits it; changing this\n\t * copy changes nothing the host records.\n\t */\n\treadonly parameters: Readonly<Record<string, unknown>>;\n\t/**\n\t * Aborted when the host's admission deadline passes. Hosts pass an\n\t * `AbortSignal`. `@fabricorg/platform-host` refuses an answer that settles\n\t * at or after the deadline, even one that settles before the abort.\n\t */\n\treadonly signal: ActionAdmissionSignal;\n}\n\n/**\n * The part of an `AbortSignal` an admission check can rely on. Declared\n * structurally so this module compiles without DOM or Node type libraries; a\n * host passes a real `AbortSignal`, which satisfies it.\n */\nexport interface ActionAdmissionSignal {\n\treadonly aborted: boolean;\n\taddEventListener(type: \"abort\", listener: () => void, options?: { once?: boolean }): void;\n\tremoveEventListener(type: \"abort\", listener: () => void): void;\n}\n\n/**\n * An admission check's answer. Only `{ admitted: true }` admits. A refusal's\n * `code` must be a fixed code chosen by the action (lower-case first letter,\n * then letters, digits, `_`, `.`, `:` or `-`, at most 128 characters), never\n * a submitted value: the host puts it in the error it throws.\n */\nexport type ActionAdmissionDecision =\n\t| { readonly admitted: true }\n\t| { readonly admitted: false; readonly code: string };\n\n/**\n * Decides whether a submission may be recorded at all. A host that supports\n * it runs the check after authorizing the submitter and before it writes\n * anything durable for the submission; a refusal, a throw, a timeout or any\n * answer other than `{ admitted: true }` refuses the submission.\n */\nexport type ActionAdmission = (input: ActionAdmissionInput) => Promise<ActionAdmissionDecision>;\n\nexport interface ActionResult {\n\tsuccess: boolean;\n\tdata?: Record<string, unknown>;\n\terror?: string;\n}\n\nexport type ActionHandler<TDb = unknown> = (\n\tctx: ActionContext<TDb>,\n\tparams: unknown,\n) => Promise<ActionResult>;\n\nexport interface AdapterStepRetryPolicy {\n\tidempotent: boolean;\n\tmaxAttempts?: number;\n\tinitialDelayMs?: number;\n\tbackoffMultiplier?: number;\n\tmaxDelayMs?: number;\n}\n\nexport interface AdapterStep {\n\tadapterType: string;\n\toperation: string;\n\tretryPolicy?: AdapterStepRetryPolicy;\n\tgetInput: (\n\t\tparams: unknown,\n\t\thandlerResult: Record<string, unknown>,\n\t) => Record<string, unknown> | undefined;\n}\n\nexport interface ActionStateMachineBinding {\n\tentityType: string;\n\ttargetState: string | ((params: unknown) => string);\n\tgetEntityId: (params: unknown) => string | undefined;\n}\n\nexport interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {\n\tactionId: ActionId;\n\tnamespace: string;\n\tversion: number;\n\tschema: SchemaValidator;\n\t/**\n\t * Optional check a host runs on every submission before it records\n\t * anything durable for it (invocation, idempotency key, events, errors).\n\t * Use it when parameters must be validated against trusted context, for\n\t * example a secret reference the tenant's secret store must hold, before\n\t * they may be persisted. `@fabricorg/platform-host` runs it from the\n\t * release that introduced it; an older host ignores it, so a handler that\n\t * relies on it should require `ActionContext.admittedAt`.\n\t */\n\tadmitSubmission?: ActionAdmission;\n\t/** Optional validation contract for the public handler result data. */\n\tresultSchema?: SchemaValidator;\n\thandler?: ActionHandler<TDb>;\n\trequiredRoles?: string[];\n\trequiredPermissions?: string[];\n\tpolicies?: `${string}.v${number}`[];\n\temitsEvents: string[];\n\tidempotent: boolean;\n\tmutatesDomain: boolean;\n\t/** Portable execution semantics used by hosts, gateways, and generated capability contracts. */\n\texecution?: ActionExecutionContract;\n\tstateMachine?: ActionStateMachineBinding;\n\tadapterSteps?: AdapterStep[];\n\t/** Completion actions can attest only after all external adapter work succeeds. */\n\teventPhase?: \"before_adapters\" | \"after_adapters\";\n\t/**\n\t * If \"saga\", the action is a multi-step orchestration. The worker dispatches\n\t * to a registered saga implementation instead of running `handler` directly.\n\t */\n\tkind?: \"atomic\" | \"saga\";\n}\n\nexport type ActionConnectivity = \"online-required\" | \"offline-capturable\";\nexport type AuthorityMoment = \"capture\" | \"execution\" | \"both\";\nexport type ActionIdempotencyStance = \"required\" | \"supported\" | \"prohibited\";\nexport type ActionCompletionMode = \"immediate\" | \"accepted\" | \"long-running\";\nexport type ActionConsistency = \"authoritative-now\" | \"provisional-until-reconciled\";\nexport type ActionDataSensitivity = \"public\" | \"internal\" | \"confidential\" | \"restricted\";\n\nexport interface ActionExecutionContract {\n\tconnectivity: ActionConnectivity;\n\tauthorityMoment: AuthorityMoment;\n\tidempotency: ActionIdempotencyStance;\n\t/**\n\t * `immediate`: the invocation settles within the host's execution.\n\t * `accepted` and `long-running`: an adapter may hand the work to an\n\t * external system and the invocation stays running until that system\n\t * completes it by reference through the host's completion ingress.\n\t * Declare `completionDeadlineMs` on a `long-running` action so a callback\n\t * that never comes is noticed rather than waited on forever.\n\t */\n\tcompletion: ActionCompletionMode;\n\t/**\n\t * How long an accepted handoff may wait for its completion before the host\n\t * moves the invocation to reconciliation. Without it, nothing sweeps the\n\t * handoff. Refused on `immediate`, since nothing external completes it.\n\t */\n\tcompletionDeadlineMs?: number;\n\tconsistency: ActionConsistency;\n\tsensitivity?: ActionDataSensitivity;\n}\n\nexport interface SagaContext {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tparameters: unknown;\n\tsleep?: (delayMs: number) => Promise<void>;\n\tquery?: <T = unknown>(model: string, operation: string, args: unknown) => Promise<T>;\n\t/**\n\t * Wait for a named external signal carrying a payload of `T`, with a timeout.\n\t *\n\t * Resolves with the payload of the first signal received, or `null` if the timeout\n\t * fires first. Subsequent distinct signals with the same name received before\n\t * another `awaitSignal` call are buffered in FIFO order so none is lost.\n\t *\n\t * A signal may carry a stable signal ID that the sender keeps the same on each redelivery\n\t * (for example a webhook delivery ID or an approval decision ID). A runtime drops\n\t * a signal whose ID it has already received for this saga, so a sender retrying\n\t * after a timeout does not wake the saga twice. Signals without an ID are not\n\t * deduplicated. `@fabricorg/platform-temporal` provides `createSagaSignalInbox()`\n\t * for Temporal runtimes.\n\t *\n\t * Intended for agent-driven and webhook-driven coordination: replace polling loops\n\t * with push-based wakeups while preserving the saga's deterministic replay\n\t * semantics (the runtime is responsible for capturing signals into workflow history).\n\t *\n\t * Optional. Runtimes that cannot implement push-based signaling (e.g. an in-memory\n\t * test harness) may omit this; sagas should treat absence as a fallback condition\n\t * and use `sleep` + `query` polling instead.\n\t *\n\t * Signal *senders* are runtime-specific (e.g. a Temporal `WorkflowClient.signal()`\n\t * call from an action handler). Apps that need to send signals from action\n\t * handlers should expose a coordination service via `ActionContext.services`\n\t * — the platform does not prescribe the sender API.\n\t *\n\t * @example\n\t * const payload = await ctx.awaitSignal?.<{ result: Record<string, unknown> }>(\n\t * \"userInputProcessed\",\n\t * 5 * 60 * 1000,\n\t * );\n\t * if (payload) { /* handle pushed result *\\/ }\n\t * else { /* timeout — fall back to deterministic path *\\/ }\n\t */\n\tawaitSignal?: <T = unknown>(signalName: string, timeoutMs: number) => Promise<T | null>;\n}\n\nexport interface SagaChildActionInput {\n\tsuffix: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n}\n\nexport type SagaRunChild = (input: SagaChildActionInput) => Promise<Record<string, unknown>>;\n\nexport interface SagaCompletionEvent {\n\teventType: string;\n\tsubjectType: string;\n\tsubjectId: string;\n\tpayload: unknown;\n\teventSchemaVersion?: number;\n}\n\nexport interface SagaCompletion {\n\tresultData: Record<string, unknown>;\n\tevent?: SagaCompletionEvent;\n}\n\nexport type SagaImplementation = (\n\tctx: SagaContext,\n\trunChild: SagaRunChild,\n) => Promise<SagaCompletion>;\n\nexport function assertActionId(actionId: string): asserts actionId is ActionId {\n\tif (!/^[a-z][a-z0-9-]*\\.[a-z][a-z0-9_]*$/.test(actionId)) {\n\t\tthrow new Error(`Invalid action ID: ${actionId}`);\n\t}\n}\n\nexport interface ActionRegistry<TDb = unknown> {\n\tregister(definition: ActionDefinition<TDb>): void;\n\tregisterIfMissing(definition: ActionDefinition<TDb>): void;\n\tresolve(actionId: ActionId): ActionDefinition<TDb> | undefined;\n\thas(actionId: string): actionId is ActionId;\n\tids(): ActionId[];\n}\n\nexport function createActionRegistry<TDb = unknown>(definitions: readonly ActionDefinition<TDb>[] = []): ActionRegistry<TDb> {\n\tconst actions = new Map<ActionId, ActionDefinition<TDb>>();\n\tconst register = (definition: ActionDefinition<TDb>): void => {\n\t\tassertActionId(definition.actionId);\n\t\tif (actions.has(definition.actionId)) throw new Error(`Action \"${definition.actionId}\" is already registered`);\n\t\tif (definition.mutatesDomain && definition.emitsEvents.length === 0) {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`);\n\t\t}\n\t\tif (definition.admitSubmission !== undefined && typeof definition.admitSubmission !== \"function\") {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" declares admitSubmission that is not a function.`);\n\t\t}\n\t\tactions.set(definition.actionId, definition);\n\t};\n\tfor (const definition of definitions) register(definition);\n\treturn {\n\t\tregister,\n\t\tregisterIfMissing(definition) {\n\t\t\tif (!actions.has(definition.actionId)) register(definition);\n\t\t},\n\t\tresolve: (actionId) => actions.get(actionId),\n\t\thas: (actionId): actionId is ActionId => actions.has(actionId as ActionId),\n\t\tids: () => Array.from(actions.keys()),\n\t};\n}\n"]}
@@ -0,0 +1,524 @@
1
+ // adapters/circuit-breaker.ts
2
+ function classifyCircuitBreakerOutcome(kind) {
3
+ switch (kind) {
4
+ case "success":
5
+ return "success";
6
+ case "permanent_failure":
7
+ return "ignored";
8
+ case "transient_failure":
9
+ case "timeout":
10
+ case "ambiguous":
11
+ return "failure";
12
+ default:
13
+ return "failure";
14
+ }
15
+ }
16
+ var MANUAL_OVERRIDES = ["force_open", "force_closed"];
17
+ function assertAdapterCircuitBreakerConfig(config) {
18
+ if (!config || typeof config !== "object") {
19
+ throw new RangeError("Adapter circuit breaker config must be an object.");
20
+ }
21
+ if (config.mode !== "monitor" && config.mode !== "enforce") {
22
+ throw new RangeError(`Adapter circuit breaker mode must be "monitor" or "enforce"; got ${JSON.stringify(config.mode)}.`);
23
+ }
24
+ if (!Number.isInteger(config.failureThreshold) || config.failureThreshold < 1) {
25
+ throw new RangeError(`Adapter circuit breaker failureThreshold must be a positive integer; got ${String(config.failureThreshold)}.`);
26
+ }
27
+ if (typeof config.windowMs !== "number" || !Number.isFinite(config.windowMs) || config.windowMs <= 0) {
28
+ throw new RangeError(`Adapter circuit breaker windowMs must be a positive number of milliseconds; got ${String(config.windowMs)}.`);
29
+ }
30
+ if (typeof config.cooldownMs !== "number" || !Number.isFinite(config.cooldownMs) || config.cooldownMs < 0) {
31
+ throw new RangeError(`Adapter circuit breaker cooldownMs must be a non-negative number of milliseconds; got ${String(config.cooldownMs)}.`);
32
+ }
33
+ if (config.minimumSamples !== void 0 && (!Number.isInteger(config.minimumSamples) || config.minimumSamples < 1)) {
34
+ throw new RangeError(`Adapter circuit breaker minimumSamples must be a positive integer; got ${String(config.minimumSamples)}.`);
35
+ }
36
+ if (config.probeLeaseMs !== void 0 && (typeof config.probeLeaseMs !== "number" || !Number.isFinite(config.probeLeaseMs) || config.probeLeaseMs <= 0)) {
37
+ throw new RangeError(`Adapter circuit breaker probeLeaseMs must be a positive number of milliseconds; got ${String(config.probeLeaseMs)}.`);
38
+ }
39
+ if (config.manualOverride !== void 0 && !MANUAL_OVERRIDES.includes(config.manualOverride)) {
40
+ throw new RangeError(`Adapter circuit breaker manualOverride must be "force_open" or "force_closed"; got ${JSON.stringify(config.manualOverride)}.`);
41
+ }
42
+ }
43
+ var InMemoryAdapterCircuitBreaker = class {
44
+ key;
45
+ mode;
46
+ config;
47
+ now;
48
+ onEvent;
49
+ probeLeaseMs;
50
+ samples = [];
51
+ state = "closed";
52
+ openedAt = 0;
53
+ probeIssuedAt;
54
+ /** Advances on every state change, manual override change and probe grant. */
55
+ generation = 0;
56
+ manualOverride;
57
+ /** Telemetry waiting for the current operation to commit. */
58
+ outbox = [];
59
+ operationDepth = 0;
60
+ flushing = false;
61
+ constructor(options) {
62
+ assertAdapterCircuitBreakerConfig(options.config);
63
+ this.key = options.key;
64
+ this.config = { ...options.config };
65
+ this.mode = options.config.mode;
66
+ this.now = options.now ?? Date.now;
67
+ this.onEvent = options.onEvent;
68
+ this.manualOverride = options.config.manualOverride;
69
+ this.probeLeaseMs = options.config.probeLeaseMs ?? Math.max(options.config.cooldownMs, options.config.windowMs);
70
+ }
71
+ getState() {
72
+ this.settle(this.now());
73
+ return this.reportedState();
74
+ }
75
+ getManualOverride() {
76
+ return this.manualOverride;
77
+ }
78
+ /**
79
+ * Set (`force_open` / `force_closed`) or clear (`undefined`) the manual
80
+ * override. Clearing returns the breaker to `closed` with its window intact.
81
+ */
82
+ setManualOverride(override) {
83
+ if (override !== void 0 && !MANUAL_OVERRIDES.includes(override)) {
84
+ throw new RangeError(`Adapter circuit breaker manualOverride must be "force_open" or "force_closed"; got ${JSON.stringify(override)}.`);
85
+ }
86
+ const now = this.now();
87
+ this.settle(now);
88
+ if (override === this.manualOverride) return;
89
+ this.operation(() => {
90
+ const from = this.reportedState();
91
+ this.manualOverride = override;
92
+ this.generation++;
93
+ if (override === void 0) {
94
+ this.state = "closed";
95
+ this.releaseProbe();
96
+ }
97
+ this.emit({
98
+ type: "state_change",
99
+ key: this.key,
100
+ mode: this.mode,
101
+ at: now,
102
+ from,
103
+ to: this.reportedState(),
104
+ reason: override === void 0 ? "manual_override_cleared" : "manual_override",
105
+ ...override ? { manualOverride: override } : {}
106
+ });
107
+ });
108
+ }
109
+ check() {
110
+ return this.decide(false, this.now());
111
+ }
112
+ acquire() {
113
+ const now = this.now();
114
+ this.settle(now);
115
+ return this.operation(() => this.decide(true, now));
116
+ }
117
+ record(outcome, admission) {
118
+ const now = this.now();
119
+ this.settle(now);
120
+ this.operation(() => this.applyOutcome(outcome, admission, now));
121
+ }
122
+ applyOutcome(outcome, admission, now) {
123
+ const generation = admission?.generation;
124
+ if (generation !== void 0 && generation !== this.generation) return;
125
+ const resolvesProbe = this.state === "half_open" && this.probeIssuedAt !== void 0 && (generation === void 0 || admission?.probe === true);
126
+ if (outcome === "ignored") {
127
+ if (resolvesProbe) this.releaseProbe();
128
+ return;
129
+ }
130
+ this.prune(now);
131
+ this.samples.push({ at: now, failure: outcome === "failure" });
132
+ if (this.manualOverride) return;
133
+ if (resolvesProbe) {
134
+ this.releaseProbe();
135
+ if (outcome === "success") {
136
+ this.samples = [];
137
+ this.transition("closed", "probe_succeeded", now);
138
+ } else {
139
+ this.openedAt = now;
140
+ this.transition("open", "probe_failed", now);
141
+ }
142
+ return;
143
+ }
144
+ if (this.state === "closed" && outcome === "failure" && this.shouldTrip()) {
145
+ this.openedAt = now;
146
+ this.transition("open", "failure_threshold", now);
147
+ }
148
+ }
149
+ /**
150
+ * `take` is false for {@link check}: a pure preview that sees an elapsed
151
+ * cooldown without committing it, takes no probe and emits nothing.
152
+ */
153
+ decide(take, now) {
154
+ if (this.manualOverride === "force_closed") {
155
+ return { allowed: true, state: "closed", probe: false, manualOverride: "force_closed", generation: this.generation };
156
+ }
157
+ if (this.manualOverride === "force_open") {
158
+ return this.refuse(take, { state: "open", manualOverride: "force_open" });
159
+ }
160
+ const cooldownElapsed = this.state === "open" && now - this.openedAt >= this.config.cooldownMs;
161
+ if (take) this.advance(now);
162
+ const state = !take && cooldownElapsed ? "half_open" : this.state;
163
+ if (state === "closed") return { allowed: true, state: "closed", probe: false, generation: this.generation };
164
+ if (state === "open") {
165
+ return this.refuse(take, {
166
+ state: "open",
167
+ retryAfterMs: Math.max(0, this.openedAt + this.config.cooldownMs - now)
168
+ });
169
+ }
170
+ const probeFree = !take && cooldownElapsed || this.probeIssuedAt === void 0 || now - this.probeIssuedAt >= this.probeLeaseMs;
171
+ if (!probeFree) return this.refuse(take, { state: "half_open" });
172
+ if (!take) return { allowed: true, state: "half_open", probe: true, generation: this.generation };
173
+ this.probeIssuedAt = now;
174
+ this.generation++;
175
+ return { allowed: true, state: "half_open", probe: true, generation: this.generation };
176
+ }
177
+ refuse(take, detail) {
178
+ const enforce = this.mode === "enforce";
179
+ if (take) {
180
+ this.emit({
181
+ type: enforce ? "rejected" : "would_reject",
182
+ key: this.key,
183
+ mode: this.mode,
184
+ at: this.now(),
185
+ ...detail
186
+ });
187
+ }
188
+ const generation = this.generation;
189
+ return enforce ? { allowed: false, probe: false, ...detail, generation } : { allowed: true, probe: false, wouldReject: true, ...detail, generation };
190
+ }
191
+ releaseProbe() {
192
+ this.probeIssuedAt = void 0;
193
+ }
194
+ /** Apply the time-based open → half_open transition. */
195
+ advance(now) {
196
+ if (this.state === "open" && now - this.openedAt >= this.config.cooldownMs) {
197
+ this.releaseProbe();
198
+ this.transition("half_open", "cooldown_elapsed", now);
199
+ }
200
+ }
201
+ shouldTrip() {
202
+ const minimumSamples = this.config.minimumSamples ?? this.config.failureThreshold;
203
+ const total = this.samples.length;
204
+ if (total < minimumSamples) return false;
205
+ let consecutive = 0;
206
+ for (let index = total - 1; index >= 0 && this.samples[index].failure; index--) consecutive++;
207
+ if (consecutive >= this.config.failureThreshold) return true;
208
+ const failures = this.samples.reduce((count, sample) => count + (sample.failure ? 1 : 0), 0);
209
+ return failures / total >= this.config.failureThreshold / minimumSamples;
210
+ }
211
+ prune(now) {
212
+ const cutoff = now - this.config.windowMs;
213
+ let drop = 0;
214
+ while (drop < this.samples.length && this.samples[drop].at < cutoff) drop++;
215
+ if (drop > 0) this.samples.splice(0, drop);
216
+ }
217
+ transition(to, reason, at) {
218
+ const from = this.state;
219
+ if (from === to) return;
220
+ this.state = to;
221
+ this.generation++;
222
+ this.emit({ type: "state_change", key: this.key, mode: this.mode, at, from, to, reason });
223
+ }
224
+ reportedState() {
225
+ if (this.manualOverride) return this.manualOverride === "force_open" ? "open" : "closed";
226
+ return this.state;
227
+ }
228
+ /**
229
+ * Commit an elapsed cooldown as an operation of its own, delivering its
230
+ * event before the caller's operation reads state.
231
+ */
232
+ settle(now) {
233
+ if (this.manualOverride) return;
234
+ this.operation(() => this.advance(now));
235
+ }
236
+ /**
237
+ * Run `step` to completion, then deliver the telemetry it produced. The
238
+ * return value is fixed before any listener runs.
239
+ */
240
+ operation(step) {
241
+ this.operationDepth++;
242
+ try {
243
+ return step();
244
+ } finally {
245
+ this.operationDepth--;
246
+ if (this.operationDepth === 0) this.flush();
247
+ }
248
+ }
249
+ emit(event) {
250
+ if (this.onEvent) this.outbox.push(event);
251
+ }
252
+ /**
253
+ * Deliver buffered events in order. A listener that calls back into the
254
+ * breaker queues its own events; this loop delivers them after it returns
255
+ * instead of recursing.
256
+ */
257
+ flush() {
258
+ if (this.flushing) return;
259
+ this.flushing = true;
260
+ try {
261
+ for (let event = this.outbox.shift(); event; event = this.outbox.shift()) this.deliver(event);
262
+ } finally {
263
+ this.flushing = false;
264
+ }
265
+ }
266
+ deliver(event) {
267
+ const listener = this.onEvent;
268
+ if (!listener) return;
269
+ try {
270
+ const result = listener(event);
271
+ if (result && typeof result.then === "function") {
272
+ result.then(void 0, () => void 0);
273
+ }
274
+ } catch {
275
+ }
276
+ }
277
+ };
278
+ function scopedAdapterCircuitBreakerKey(adapter, scope) {
279
+ return [
280
+ scope === void 0 ? "unscoped" : `tenant=${encodeURIComponent(scope.tenantId)}`,
281
+ scope?.spaceId === void 0 ? "space" : `space=${encodeURIComponent(scope.spaceId)}`,
282
+ adapter.vendor === void 0 ? "vendor" : `vendor=${encodeURIComponent(adapter.vendor)}`,
283
+ encodeURIComponent(adapter.adapterType),
284
+ encodeURIComponent(adapter.operation)
285
+ ].join(":");
286
+ }
287
+ var AdapterCircuitBreakerRegistry = class {
288
+ constructor(options = {}) {
289
+ this.options = options;
290
+ }
291
+ options;
292
+ breakers = /* @__PURE__ */ new Map();
293
+ forAdapter(adapter, scope) {
294
+ if (!adapter.circuitBreaker) return void 0;
295
+ const key = this.options.key?.(adapter, scope) ?? `${adapter.adapterType}:${adapter.operation}`;
296
+ let breaker = this.breakers.get(key);
297
+ if (!breaker) {
298
+ breaker = new InMemoryAdapterCircuitBreaker({
299
+ key,
300
+ config: adapter.circuitBreaker,
301
+ ...this.options.now ? { now: this.options.now } : {},
302
+ ...this.options.onEvent ? { onEvent: this.options.onEvent } : {}
303
+ });
304
+ this.breakers.set(key, breaker);
305
+ }
306
+ return breaker;
307
+ }
308
+ /** Looks up the default `adapterType:operation` key without creating a breaker. */
309
+ get(adapterType, operation) {
310
+ return this.getByKey(`${adapterType}:${operation}`);
311
+ }
312
+ /** Looks up an exact key, including a custom key, without creating a breaker. */
313
+ getByKey(key) {
314
+ return this.breakers.get(key);
315
+ }
316
+ };
317
+ var AdapterCircuitOpenError = class extends Error {
318
+ name = "AdapterCircuitOpenError";
319
+ code = "ADAPTER_CIRCUIT_OPEN";
320
+ reason = "circuit_open";
321
+ adapterKey;
322
+ state;
323
+ /** The attempt number that was refused (1 = refused before any call). */
324
+ attempt;
325
+ retryAfterMs;
326
+ manualOverride;
327
+ /** Error of the previous attempt, when the breaker opened mid-retry. */
328
+ lastError;
329
+ constructor(input) {
330
+ const after = input.retryAfterMs !== void 0 ? `; retry after ${input.retryAfterMs} ms` : "";
331
+ super(`Adapter circuit is ${input.state === "half_open" ? "half-open with its probe in flight" : "open"} for ${input.adapterKey}; the call was not attempted${after}.`);
332
+ this.adapterKey = input.adapterKey;
333
+ this.state = input.state;
334
+ this.attempt = input.attempt;
335
+ if (input.retryAfterMs !== void 0) this.retryAfterMs = input.retryAfterMs;
336
+ if (input.manualOverride !== void 0) this.manualOverride = input.manualOverride;
337
+ if (input.lastError !== void 0) this.lastError = input.lastError;
338
+ }
339
+ };
340
+ function isAdapterCircuitOpenError(error) {
341
+ if (error instanceof AdapterCircuitOpenError) return true;
342
+ return typeof error === "object" && error !== null && error.name === "AdapterCircuitOpenError" && error.code === "ADAPTER_CIRCUIT_OPEN";
343
+ }
344
+
345
+ // adapters/index.ts
346
+ function adapterKey(adapterType, operation) {
347
+ return `${adapterType}:${operation}`;
348
+ }
349
+ var MissingAdapterRegistrationError = class extends Error {
350
+ constructor(adapterType, operation) {
351
+ super(`No adapter registered for adapterType "${adapterType}" and operation "${operation}"`);
352
+ this.name = "MissingAdapterRegistrationError";
353
+ }
354
+ };
355
+ var AdapterRegistry = class {
356
+ adapters = /* @__PURE__ */ new Map();
357
+ register(adapter) {
358
+ const key = adapterKey(adapter.adapterType, adapter.operation);
359
+ if (this.adapters.has(key)) {
360
+ throw new Error(
361
+ `Adapter already registered for adapterType "${adapter.adapterType}" and operation "${adapter.operation}"`
362
+ );
363
+ }
364
+ this.adapters.set(key, adapter);
365
+ }
366
+ resolve(adapterType, operation) {
367
+ return this.adapters.get(adapterKey(adapterType, operation));
368
+ }
369
+ require(adapterType, operation) {
370
+ const adapter = this.resolve(adapterType, operation);
371
+ if (!adapter) {
372
+ throw new MissingAdapterRegistrationError(adapterType, operation);
373
+ }
374
+ return adapter;
375
+ }
376
+ };
377
+ var DEFAULT_RETRY_POLICY = {
378
+ maxAttempts: 3,
379
+ initialDelayMs: 100,
380
+ backoffMultiplier: 2,
381
+ maxDelayMs: 1e3
382
+ };
383
+ var NO_RETRY_POLICY = {
384
+ idempotent: false,
385
+ maxAttempts: 1,
386
+ initialDelayMs: 0,
387
+ backoffMultiplier: 1,
388
+ maxDelayMs: 0
389
+ };
390
+ function resolveAdapterRetryPolicy(policy, defaultIdempotent) {
391
+ const idempotent = policy?.idempotent ?? defaultIdempotent;
392
+ if (!idempotent) {
393
+ return NO_RETRY_POLICY;
394
+ }
395
+ return {
396
+ idempotent: true,
397
+ maxAttempts: Math.max(1, Math.min(policy?.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts, 5)),
398
+ initialDelayMs: Math.max(0, policy?.initialDelayMs ?? DEFAULT_RETRY_POLICY.initialDelayMs),
399
+ backoffMultiplier: Math.max(
400
+ 1,
401
+ policy?.backoffMultiplier ?? DEFAULT_RETRY_POLICY.backoffMultiplier
402
+ ),
403
+ maxDelayMs: Math.max(0, policy?.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs)
404
+ };
405
+ }
406
+ function getRetryDelayMs(attempt, policy) {
407
+ const exponentialDelay = policy.initialDelayMs * policy.backoffMultiplier ** Math.max(0, attempt - 1);
408
+ return Math.min(exponentialDelay, policy.maxDelayMs);
409
+ }
410
+ function classifyAdapterOutcome(result) {
411
+ if (result.outcome) {
412
+ assertAdapterOutcomeConsistency(result);
413
+ return result.outcome;
414
+ }
415
+ return result.success ? "success" : "transient_failure";
416
+ }
417
+ function shouldRetryAdapterOutcome(kind, idempotent) {
418
+ return kind === "transient_failure" && idempotent;
419
+ }
420
+ function assertAdapterOutcomeConsistency(result) {
421
+ if (result.success && result.outcome && result.outcome !== "success") {
422
+ throw new Error(
423
+ `Adapter returned success=true with contradictory outcome "${result.outcome}"; a successful result must not declare a non-success outcome.`
424
+ );
425
+ }
426
+ }
427
+ function recordCircuitOutcome(breaker, outcome, admission) {
428
+ if (!breaker) return;
429
+ try {
430
+ breaker.record(outcome, admission);
431
+ } catch {
432
+ }
433
+ }
434
+ function circuitOpenError(breaker, admission, attempt, lastError) {
435
+ return new AdapterCircuitOpenError({
436
+ adapterKey: breaker.key,
437
+ state: admission.state,
438
+ attempt,
439
+ ...admission.retryAfterMs !== void 0 ? { retryAfterMs: admission.retryAfterMs } : {},
440
+ ...admission.manualOverride !== void 0 ? { manualOverride: admission.manualOverride } : {},
441
+ ...lastError !== void 0 ? { lastError } : {}
442
+ });
443
+ }
444
+ async function executeWithAdapterRetry(options) {
445
+ const policy = resolveAdapterRetryPolicy(options.policy, options.defaultIdempotent);
446
+ const sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
447
+ const breaker = options.circuitBreaker;
448
+ let lastError;
449
+ for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
450
+ let admission;
451
+ let executed = false;
452
+ let recorded = false;
453
+ const record = (outcome) => {
454
+ if (!executed || recorded) return;
455
+ recorded = true;
456
+ recordCircuitOutcome(breaker, outcome, admission);
457
+ };
458
+ let refusal;
459
+ async function handleThrown(error) {
460
+ lastError = error instanceof Error ? error.message : String(error);
461
+ const thrownOutcome = options.classifyThrownError ? options.classifyThrownError(error) : "transient_failure";
462
+ record(classifyCircuitBreakerOutcome(thrownOutcome));
463
+ let willRetry = shouldRetryAdapterOutcome(thrownOutcome, policy.idempotent) && attempt < policy.maxAttempts;
464
+ const blocked = willRetry ? refusedRetry(breaker) : void 0;
465
+ if (blocked) willRetry = false;
466
+ const nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : void 0;
467
+ await options.onAttempt?.({ attempt, error: lastError, willRetry, nextDelayMs });
468
+ if (blocked) return blocked;
469
+ if (!willRetry) return "rethrow";
470
+ await sleep(nextDelayMs ?? 0);
471
+ return "retry";
472
+ }
473
+ try {
474
+ if (options.prepareAttempt) await options.prepareAttempt(attempt, policy.maxAttempts);
475
+ } catch (error) {
476
+ const decision = await handleThrown(error);
477
+ if (decision === "rethrow") throw error;
478
+ if (decision === "retry") continue;
479
+ refusal = decision;
480
+ }
481
+ if (!refusal) {
482
+ if (breaker) {
483
+ admission = breaker.acquire();
484
+ if (!admission.allowed) throw circuitOpenError(breaker, admission, attempt, lastError);
485
+ }
486
+ try {
487
+ executed = true;
488
+ const result = await options.execute(attempt, policy.maxAttempts);
489
+ if (options.isSuccessful(result)) {
490
+ record("success");
491
+ await options.onAttempt?.({ attempt, result, willRetry: false });
492
+ return result;
493
+ }
494
+ lastError = options.getError(result) ?? "Adapter returned unsuccessful result";
495
+ const outcome = options.classifyOutcome ? options.classifyOutcome(result) : "transient_failure";
496
+ record(classifyCircuitBreakerOutcome(outcome));
497
+ let willRetry = shouldRetryAdapterOutcome(outcome, policy.idempotent) && attempt < policy.maxAttempts;
498
+ refusal = willRetry ? refusedRetry(breaker) : void 0;
499
+ if (refusal) willRetry = false;
500
+ const nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : void 0;
501
+ await options.onAttempt?.({ attempt, result, error: lastError, willRetry, nextDelayMs });
502
+ if (!willRetry && !refusal) {
503
+ return result;
504
+ }
505
+ if (!refusal) await sleep(nextDelayMs ?? 0);
506
+ } catch (error) {
507
+ const decision = await handleThrown(error);
508
+ if (decision === "rethrow") throw error;
509
+ refusal = decision === "retry" ? void 0 : decision;
510
+ }
511
+ }
512
+ if (refusal && breaker) throw circuitOpenError(breaker, refusal, attempt + 1, lastError);
513
+ }
514
+ throw new Error(lastError ?? "Adapter retry exhausted");
515
+ }
516
+ function refusedRetry(breaker) {
517
+ if (!breaker) return void 0;
518
+ const preview = breaker.check();
519
+ return preview.allowed ? void 0 : preview;
520
+ }
521
+
522
+ export { AdapterCircuitBreakerRegistry, AdapterCircuitOpenError, AdapterRegistry, InMemoryAdapterCircuitBreaker, MissingAdapterRegistrationError, assertAdapterCircuitBreakerConfig, assertAdapterOutcomeConsistency, classifyAdapterOutcome, classifyCircuitBreakerOutcome, executeWithAdapterRetry, getRetryDelayMs, isAdapterCircuitOpenError, resolveAdapterRetryPolicy, scopedAdapterCircuitBreakerKey, shouldRetryAdapterOutcome };
523
+ //# sourceMappingURL=chunk-Q4Q4JPR3.js.map
524
+ //# sourceMappingURL=chunk-Q4Q4JPR3.js.map