@eventsdk/sdk 0.0.0-stage → 0.4.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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/dist/compatibility-http/bridge/receive.d.ts +3 -0
- package/dist/compatibility-http/bridge/types.d.ts +44 -0
- package/dist/compatibility-http/index.d.ts +4 -0
- package/dist/compatibility-http/index.js +1 -0
- package/dist/compatibility-http/policy/validate-policy.d.ts +3 -0
- package/dist/conformance/admission/harness.d.ts +20 -0
- package/dist/conformance/admission/run-suite.d.ts +2 -0
- package/dist/conformance/consumer/harness.d.ts +24 -0
- package/dist/conformance/consumer/run-apply-suite.d.ts +2 -0
- package/dist/conformance/consumer/run-worker-suite.d.ts +2 -0
- package/dist/conformance/event-link/harness.d.ts +13 -0
- package/dist/conformance/event-link/run-suite.d.ts +2 -0
- package/dist/conformance/fanout/harness.d.ts +39 -0
- package/dist/conformance/fanout/run-suite.d.ts +2 -0
- package/dist/conformance/gap/harness.d.ts +19 -0
- package/dist/conformance/gap/run-suite.d.ts +2 -0
- package/dist/conformance/index.d.ts +22 -0
- package/dist/conformance/index.js +1 -0
- package/dist/conformance/producer/harness.d.ts +21 -0
- package/dist/conformance/producer/run-suite.d.ts +2 -0
- package/dist/conformance/receipt/harness.d.ts +12 -0
- package/dist/conformance/receipt/projection-harness.d.ts +9 -0
- package/dist/conformance/receipt/run-projection-suite.d.ts +2 -0
- package/dist/conformance/receipt/run-suite.d.ts +2 -0
- package/dist/conformance/reliability/checks.d.ts +4 -0
- package/dist/conformance/result/harness.d.ts +15 -0
- package/dist/conformance/result/run-suite.d.ts +2 -0
- package/dist/conformance/version/compatibility.d.ts +13 -0
- package/dist/contracts/canonical/canonical-json.d.ts +1 -0
- package/dist/contracts/contract-types.d.ts +34 -0
- package/dist/contracts/generated/business-receipt.d.ts +21 -0
- package/dist/contracts/generated/cloud-event-context.d.ts +32 -0
- package/dist/contracts/generated/cloud-event-fanout.d.ts +33 -0
- package/dist/contracts/generated/cloud-event.d.ts +24 -0
- package/dist/contracts/generated/event-link-authorization-request.d.ts +22 -0
- package/dist/contracts/generated/event-link-proposal.d.ts +27 -0
- package/dist/contracts/generated/event-link-token-request.d.ts +7 -0
- package/dist/contracts/generated/event-origin-claims.d.ts +13 -0
- package/dist/contracts/generated/exchange-grant.d.ts +11 -0
- package/dist/contracts/generated/exchange-registration.d.ts +24 -0
- package/dist/contracts/generated/fanout-event-draft.d.ts +28 -0
- package/dist/contracts/generated/gap-baseline.d.ts +18 -0
- package/dist/contracts/generated/gap-recovery-command.d.ts +7 -0
- package/dist/contracts/generated/identity-context.d.ts +15 -0
- package/dist/contracts/generated/namespaced-reference.d.ts +4 -0
- package/dist/contracts/generated/native-event-catalog.d.ts +19 -0
- package/dist/contracts/generated/node-connection-profile.d.ts +217 -0
- package/dist/contracts/generated/node-worker-observation.d.ts +7 -0
- package/dist/contracts/generated/outbox-recovery-command.d.ts +8 -0
- package/dist/contracts/generated/portable-event-link-authorization-request.d.ts +98 -0
- package/dist/contracts/generated/portable-event-link-proposal.d.ts +121 -0
- package/dist/contracts/generated/portable-event-link-token-request.d.ts +34 -0
- package/dist/contracts/generated/portable-exchange-registration.d.ts +109 -0
- package/dist/contracts/generated/portable-gap-baseline.d.ts +93 -0
- package/dist/contracts/generated/portable-gap-recovery-command.d.ts +35 -0
- package/dist/contracts/generated/portable-outbox-recovery-command.d.ts +37 -0
- package/dist/contracts/generated/public-jwks-snapshot.d.ts +141 -0
- package/dist/contracts/generated/result-ref.d.ts +8 -0
- package/dist/contracts/generated/supported-profiles.d.ts +3 -0
- package/dist/contracts/generated/tenant-connection.d.ts +13 -0
- package/dist/contracts/generated/tenant-context.d.ts +4 -0
- package/dist/contracts/index.d.ts +31 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/validation/contract-error.d.ts +16 -0
- package/dist/contracts/validation/contracts.d.ts +316 -0
- package/dist/contracts/validation/decode-cloud-event.d.ts +2 -0
- package/dist/contracts/validation/event-link.d.ts +9 -0
- package/dist/contracts/validation/identity-context.d.ts +3 -0
- package/dist/contracts/validation/origin-proof-wire.d.ts +3 -0
- package/dist/contracts/validation/strict-json.d.ts +1 -0
- package/dist/core/inbox/accept-delivery.d.ts +26 -0
- package/dist/core/inbox/apply-delivery.d.ts +29 -0
- package/dist/core/inbox/domain-apply-result.d.ts +13 -0
- package/dist/core/inbox/inbox-worker.d.ts +2 -0
- package/dist/core/inbox/read-result.d.ts +12 -0
- package/dist/core/inbox/worker-types.d.ts +21 -0
- package/dist/core/index.d.ts +50 -0
- package/dist/core/index.js +1 -0
- package/dist/core/outbox/authorize-recovery.d.ts +75 -0
- package/dist/core/outbox/outbox-relay.d.ts +26 -0
- package/dist/core/outbox/record-event.d.ts +10 -0
- package/dist/core/outbox/record-fanout.d.ts +16 -0
- package/dist/core/ports/dead-letter.d.ts +29 -0
- package/dist/core/ports/inbox.d.ts +43 -0
- package/dist/core/ports/observability.d.ts +21 -0
- package/dist/core/ports/origin-refresh.d.ts +5 -0
- package/dist/core/ports/outbox.d.ts +46 -0
- package/dist/core/ports/result-reader.d.ts +14 -0
- package/dist/core/ports/transport.d.ts +13 -0
- package/dist/core/receipt/receipt-state.d.ts +3 -0
- package/dist/core/recovery/event-log.d.ts +28 -0
- package/dist/core/recovery/gap-baseline-binding.d.ts +2 -0
- package/dist/core/recovery/gap-recovery.d.ts +15 -0
- package/dist/core/recovery/receipt-identity-recovery.d.ts +8 -0
- package/dist/core/recovery/receipt-recovery.d.ts +14 -0
- package/dist/core/recovery/replay-dead-letter.d.ts +4 -0
- package/dist/core/runtime/dependencies.d.ts +19 -0
- package/dist/core/runtime/errors.d.ts +6 -0
- package/dist/core/runtime/event-identity.d.ts +14 -0
- package/dist/core/runtime/exchange-connection.d.ts +7 -0
- package/dist/core/runtime/fanout-identity.d.ts +6 -0
- package/dist/core/runtime/lease.d.ts +6 -0
- package/dist/core/runtime/observe-health.d.ts +3 -0
- package/dist/core/runtime/operation.d.ts +2 -0
- package/dist/core/runtime/retry.d.ts +14 -0
- package/dist/event-link/errors.d.ts +5 -0
- package/dist/event-link/exchange.d.ts +9 -0
- package/dist/event-link/index.d.ts +12 -0
- package/dist/event-link/index.js +1 -0
- package/dist/event-link/primitives.d.ts +10 -0
- package/dist/event-link/protocol.d.ts +21 -0
- package/dist/event-link/receiver-ports.d.ts +42 -0
- package/dist/event-link/receiver.d.ts +18 -0
- package/dist/event-link/registration.d.ts +21 -0
- package/dist/event-link/sender.d.ts +22 -0
- package/dist/identity-contracts/authorization/authorization-port.d.ts +35 -0
- package/dist/identity-contracts/index.d.ts +2 -0
- package/dist/identity-contracts/index.js +0 -0
- package/dist/identity-contracts/origin/verified-origin.d.ts +28 -0
- package/dist/identity-verifier/index.d.ts +6 -0
- package/dist/identity-verifier/index.js +1 -0
- package/dist/identity-verifier/jwt/service-token-verifier.d.ts +15 -0
- package/dist/identity-verifier/origin/event-origin-policy.d.ts +36 -0
- package/dist/identity-verifier/origin/event-origin-signer.d.ts +6 -0
- package/dist/identity-verifier/origin/event-origin-verifier.d.ts +6 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +22 -0
- package/dist/node-config/configuration-error.d.ts +4 -0
- package/dist/node-config/diagnostics/check-configuration.d.ts +40 -0
- package/dist/node-config/identity/load-identity.d.ts +14 -0
- package/dist/node-config/identity/local-recovery-origin.d.ts +3 -0
- package/dist/node-config/identity/public-identity.d.ts +87 -0
- package/dist/node-config/identity/public-snapshot.d.ts +2 -0
- package/dist/node-config/identity/signing-identity.d.ts +5 -0
- package/dist/node-config/index.d.ts +12 -0
- package/dist/node-config/index.js +1 -0
- package/dist/node-config/main.js +3 -0
- package/dist/node-config/operations/export-own-public.d.ts +8 -0
- package/dist/node-config/operations/refresh-trust.d.ts +10 -0
- package/dist/node-config/profile/load-profile.d.ts +6 -0
- package/dist/node-config/profile/validate-profile.d.ts +2 -0
- package/dist/node-worker/index.d.ts +3 -0
- package/dist/node-worker/index.js +1 -0
- package/dist/node-worker/lifecycle.d.ts +19 -0
- package/dist/node-worker/route-scheduler.d.ts +17 -0
- package/dist/node-worker/session.d.ts +15 -0
- package/dist/provisioner/application/apply.d.ts +11 -0
- package/dist/provisioner/application/ports.d.ts +44 -0
- package/dist/provisioner/application/verify.d.ts +7 -0
- package/dist/provisioner/cli/arguments.d.ts +9 -0
- package/dist/provisioner/cli/live-command.d.ts +7 -0
- package/dist/provisioner/cli/main.js +2 -0
- package/dist/provisioner/cli/run.d.ts +2 -0
- package/dist/provisioner/cli/session-types.d.ts +26 -0
- package/dist/provisioner/conformance/policy.d.ts +9 -0
- package/dist/provisioner/index.d.ts +17 -0
- package/dist/provisioner/index.js +1 -0
- package/dist/provisioner/manifest/types.d.ts +55 -0
- package/dist/provisioner/manifest/validate.d.ts +3 -0
- package/dist/provisioner/nats/inspector.d.ts +13 -0
- package/dist/provisioner/nats/types.d.ts +38 -0
- package/dist/provisioner/nats/writer-types.d.ts +11 -0
- package/dist/provisioner/nats/writer.d.ts +18 -0
- package/dist/provisioner/planning/digest.d.ts +1 -0
- package/dist/provisioner/planning/plan.d.ts +18 -0
- package/dist/transport-nats/connection/delivery-session.d.ts +10 -0
- package/dist/transport-nats/connection/verified-connection.d.ts +12 -0
- package/dist/transport-nats/consume/delivery-handler.d.ts +34 -0
- package/dist/transport-nats/consume/nats-consumer.d.ts +18 -0
- package/dist/transport-nats/index.d.ts +12 -0
- package/dist/transport-nats/index.js +3 -0
- package/dist/transport-nats/publish/nats-publisher.d.ts +24 -0
- package/dist/transport-nats/transport-error.d.ts +5 -0
- package/licenses/NATS-LICENSE +201 -0
- package/package.json +79 -3
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var EventSdkConfigurationError=class extends Error{constructor(code){super(code);this.code=code}code};import{isIP}from"node:net";import{isAbsolute}from"node:path";import{parseNodeConnectionProfile}from"@eventsdk/sdk/contracts";function validateNodeProfile(raw,expectedServiceRef){let profile;try{profile=parseNodeConnectionProfile(structuredClone(raw))}catch{throw new EventSdkConfigurationError("PROFILE_INVALID")}if(expectedServiceRef&&profile.serviceRef!==expectedServiceRef)fail("SERVICE_IDENTITY_MISMATCH");broker(profile);identity(profile);const source=new URL(profile.installationSource);if(source.username||source.password||source.search||source.hash)fail("INSTALLATION_SOURCE_INVALID");if(profile.publisher&&(!profile.identity.signer||!subject(profile.publisher.subject)||!entity(profile.publisher.expectedStream)))fail("PUBLISHER_IDENTITY_REQUIRED");if(profile.consumer&&(!profile.identity.trustedSources.length||!subject(profile.consumer.subject)||!entity(profile.consumer.stream)||!entity(profile.consumer.durableName)))fail("CONSUMER_IDENTITY_REQUIRED");if(!profile.publisher&&!profile.consumer)fail("CAPABILITY_ROLE_REQUIRED");routing(profile);return deepFreeze(profile)}function routing(profile){const publisher=profile.publisher;const audiences=profile.identity.signer?.audiences??[];if(publisher){const routes=publisher.audienceRoutes;if(!routes&&audiences.length!==1)fail("PUBLISHER_AUDIENCE_ROUTES_REQUIRED");if(routes&&(routes.length!==audiences.length||new Set(routes.map(r=>r.audience)).size!==routes.length||new Set(routes.map(r=>r.subject)).size!==routes.length||routes.some(r=>!audiences.includes(r.audience)||!subject(r.subject)||!entity(r.expectedStream))||routes[0]?.subject!==publisher.subject||routes[0]?.expectedStream!==publisher.expectedStream))fail("PUBLISHER_AUDIENCE_ROUTES_INVALID")}const consumer=profile.consumer;if(consumer){const bindings=[consumer,...consumer.additionalBindings??[]];if(new Set(bindings.map(b=>JSON.stringify([b.stream,b.durableName]))).size!==bindings.length||new Set(bindings.map(b=>b.subject)).size!==bindings.length||bindings.some(b=>!subject(b.subject)||!entity(b.stream)||!entity(b.durableName))||bindings.length*consumer.batchSize*profile.broker.timeoutMs>3e5)fail("CONSUMER_BINDINGS_INVALID")}}function broker(profile){const b=profile.broker,url=new URL(b.serverUrl);if(url.protocol!=="tls:"||url.username||url.password||url.search||url.hash||!["","/"].includes(url.pathname)||isIP(url.hostname.replace(/^\[|\]$/g,""))||url.hostname.length>253||url.hostname.split(".").some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))||b.expectedDnsName!==url.hostname)fail("BROKER_TLS_IDENTITY_REQUIRED");files([b.credentialsFile,...b.caFile?[b.caFile]:[]]);if(profile.consumer&&(profile.consumer.fetchExpiresMs>=b.timeoutMs||profile.consumer.batchSize*b.timeoutMs>3e5))fail("CONSUMER_BOUNDS_INVALID")}function identity(profile){const signer=profile.identity.signer;if(signer){https(signer.issuer);https(signer.jwksUri);files([signer.privateKeyFile,signer.publicSnapshotFile])}const seen=new Set;for(const trust of profile.identity.trustedSources){https(trust.issuer);https(trust.jwksUri);files([trust.publicSnapshotFile]);const key=JSON.stringify([trust.source,trust.serviceRef,trust.audience]);if(seen.has(key)||trust.audience!==profile.serviceRef)fail("TRUST_BINDING_AMBIGUOUS");seen.add(key)}}function https(value){const url=new URL(value);if(url.protocol!=="https:"||url.username||url.password||url.search||url.hash)fail("ISSUER_HTTPS_REQUIRED")}function files(values){if(values.some(path=>!isAbsolute(path)||path.includes("\0")))fail("FILE_REFERENCE_INVALID")}function subject(value){return/^[^\s.*>]+(?:\.[^\s.*>]+)*$/u.test(value)}function entity(value){return/^[^\s.*>/\\]+$/u.test(value)}function fail(code){throw new EventSdkConfigurationError(code)}function deepFreeze(value){if(value&&typeof value==="object"&&!Object.isFrozen(value)){Object.freeze(value);Object.values(value).forEach(deepFreeze)}return value}import{createHash}from"node:crypto";import{canonicalJson,parseJsonStrict}from"@eventsdk/sdk/contracts";import{constants}from"node:fs";import{open}from"node:fs/promises";import{isAbsolute as isAbsolute2}from"node:path";async function readConfigurationFile(path,privateFile=false){if(!isAbsolute2(path)||path.includes("\0"))throw new EventSdkConfigurationError("FILE_REFERENCE_INVALID");try{const file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);try{const stat=await file.stat();if(!stat.isFile()||stat.nlink!==1||stat.size<1||stat.size>65536||(stat.mode&18)!==0||privateFile&&(stat.mode&63)!==0)throw new EventSdkConfigurationError("FILE_PROTECTION_REQUIRED");const bytes=Buffer.alloc(65537);const read=await file.read(bytes,0,bytes.length,0);if(read.bytesRead>65536||read.bytesRead!==stat.size)throw new EventSdkConfigurationError("FILE_CHANGED");const after=await file.stat();if(after.size!==stat.size||after.mtimeMs!==stat.mtimeMs||after.ctimeMs!==stat.ctimeMs||after.nlink!==1)throw new EventSdkConfigurationError("FILE_CHANGED");return new TextDecoder("utf-8",{fatal:true}).decode(bytes.subarray(0,read.bytesRead))}finally{await file.close()}}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("FILE_UNAVAILABLE")}}async function loadNodeConnectionProfile(path,expectedServiceRef){const text=await readConfigurationFile(path);try{const profile=validateNodeProfile(parseJsonStrict(text),expectedServiceRef);return Object.freeze({profile,revision:`sha256:${createHash("sha256").update(canonicalJson(profile)).digest("hex")}`})}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("PROFILE_INVALID")}}import{parsePublicJwksSnapshot,parseJsonStrict as parseJsonStrict2}from"@eventsdk/sdk/contracts";import{validatePublicOriginKeys}from"@eventsdk/sdk/identity-verifier";async function loadPublicTrustSnapshot(file,issuer,jwksUri,now=Date.now()){try{const snapshot=parsePublicJwksSnapshot(parseJsonStrict2(await readConfigurationFile(file)));const fetched=Date.parse(snapshot.fetchedAt),until=Date.parse(snapshot.validUntil);if(snapshot.issuer!==issuer||jwksUri&&snapshot.jwksUri!==jwksUri)fail2("TRUST_ISSUER_MISMATCH");const uri=new URL(snapshot.jwksUri);if(uri.protocol!=="https:"||uri.username||uri.password||uri.search||uri.hash)fail2("TRUST_URI_INVALID");if(!Number.isFinite(now)||fetched>now||now>=until||until<=fetched||until-fetched>864e5)fail2("TRUST_SNAPSHOT_EXPIRED");validatePublicOriginKeys(snapshot.jwks);return snapshot}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("TRUST_SNAPSHOT_INVALID")}}function fail2(code){throw new EventSdkConfigurationError(code)}async function loadNodePublicIdentity(file,expectedServiceRef,now=Date.now()){if(!file)return null;const{profile}=await loadNodeConnectionProfile(file,expectedServiceRef),signer=profile.identity.signer;if(!signer)return null;const snapshot=await loadPublicTrustSnapshot(signer.publicSnapshotFile,signer.issuer,signer.jwksUri,now);return{schemaVersion:1,profile:profile.identity.originProof,serviceRef:profile.serviceRef,source:profile.installationSource,issuer:signer.issuer,subject:signer.subject,jwksUri:snapshot.jwksUri,validUntil:snapshot.validUntil,jwks:snapshot.jwks}}async function loadNodeLocalRecoveryOrigin(file,serviceRef,event){const now=Date.now(),{profile}=await loadNodeConnectionProfile(file,serviceRef);const identity2=await loadNodePublicIdentity(file,serviceRef,now);if(!identity2||profile.mode!=="ACTIVE"||identity2.source!==event.source||identity2.serviceRef!==event.serviceorigin||!profile.identity.signer?.audiences.includes(event.audience))throw new EventSdkConfigurationError("LOCAL_RECOVERY_IDENTITY_MISMATCH");return{serviceRef,issuer:identity2.issuer,subject:identity2.subject,audience:[event.audience],verifiedAt:new Date(now).toISOString(),expiresAt:identity2.validUntil}}import{createPublicKey}from"node:crypto";import{createEventOriginSigner}from"@eventsdk/sdk/identity-verifier";async function loadSigningIdentity(profile,clock){const config=profile.identity.signer;if(!config)return null;const snapshot=await loadPublicTrustSnapshot(config.publicSnapshotFile,config.issuer,config.jwksUri,clock().getTime());const privateKeyPkcs8=await readConfigurationFile(config.privateKeyFile,true);assertKeyPair(config,snapshot.jwks.keys,privateKeyPkcs8);try{return await createEventOriginSigner({issuer:config.issuer,subject:config.subject,serviceRef:profile.serviceRef,source:profile.installationSource,keyId:config.keyId,algorithm:config.algorithm,privateKeyPkcs8,audiences:config.audiences,keysValidUntil:snapshot.validUntil,clock})}catch{throw new EventSdkConfigurationError("SIGNING_IDENTITY_INVALID")}}function assertKeyPair(config,keys,pem){try{const expected=keys.find(key=>key.kid===config.keyId);const actual=createPublicKey(pem).export({format:"jwk"});if(!expected||expected.alg!==void 0&&expected.alg!==config.algorithm||expected.kty!==actual.kty)throw new Error("mismatch");const fields=actual.kty==="RSA"?["n","e"]:actual.kty==="EC"?["crv","x","y"]:["crv","x"];if(fields.some(field=>expected[field]!==actual[field]))throw new Error("mismatch")}catch{throw new EventSdkConfigurationError("SIGNING_KEY_PAIR_MISMATCH")}}import{createEventOriginVerifier}from"@eventsdk/sdk/identity-verifier";async function loadTrustedSources(profile,clock){for(const binding of profile.identity.trustedSources)await verifier(binding,clock);return async(event,proof,signal)=>{signal.throwIfAborted();const selected=profile.identity.trustedSources.find(binding=>binding.source===event.source&&binding.serviceRef===event.serviceorigin&&binding.audience===event.audience);if(!selected)return{verified:false,reasonCode:"SOURCE_NOT_ENROLLED"};try{return await(await verifier(selected,clock)).verify(event,proof,signal)}catch(error){return{verified:false,reasonCode:signal.aborted?"CANCELLED":error instanceof EventSdkConfigurationError?error.code:"TRUST_SNAPSHOT_INVALID"}}}}async function verifier(binding,clock){const snapshot=await loadPublicTrustSnapshot(binding.publicSnapshotFile,binding.issuer,binding.jwksUri,clock().getTime());return createEventOriginVerifier({source:binding.source,serviceRef:binding.serviceRef,issuer:binding.issuer,subject:binding.subject,audience:binding.audience,algorithms:binding.algorithms,jwks:snapshot.jwks,keysValidUntil:snapshot.validUntil,clock})}async function loadNodeConnectionIdentity(loaded,clock=()=>new Date,inspectionOnly=false){if(loaded.profile.mode==="DISABLED"&&!inspectionOnly)throw new EventSdkConfigurationError("PROFILE_DISABLED");const config=loaded.profile.broker;await readConfigurationFile(config.credentialsFile,true);if(config.caFile)await readConfigurationFile(config.caFile);const signer=await loadSigningIdentity(loaded.profile,clock);const verifyOrigin=await loadTrustedSources(loaded.profile,clock);return Object.freeze({profile:loaded.profile,revision:loaded.revision,signer,verifyOrigin,async withBrokerCredentials(run){const credential=await readConfigurationFile(config.credentialsFile,true);const ca=config.caFile?await readConfigurationFile(config.caFile):void 0;return run({credential:new TextEncoder().encode(credential),...ca?{ca}:{}})}})}async function checkNodeConnectionConfiguration(file,expectedServiceRef,checkFiles=false){if(!file)return{status:"DISABLED",configured:false,connected:false};try{const loaded=await loadNodeConnectionProfile(file,expectedServiceRef),p=loaded.profile;if(checkFiles)await loadNodeConnectionIdentity(loaded,()=>new Date,true);return{status:p.mode==="DISABLED"?"DISABLED":checkFiles?"ASSETS_VALID":"PROFILE_VALID",configured:true,connected:false,serviceRef:p.serviceRef,mode:p.mode,revision:loaded.revision,roles:{publisher:p.publisher!==null,consumer:p.consumer!==null},originProof:p.identity.originProof,trustedSources:p.identity.trustedSources.length,filesChecked:checkFiles}}catch(error){return{status:"INVALID",configured:true,connected:false,code:error instanceof EventSdkConfigurationError?error.code:"CONFIGURATION_UNAVAILABLE"}}}import{createPrivateKey,createPublicKey as createPublicKey2}from"node:crypto";import{parseJsonStrict as parseJsonStrict3,parsePublicJwksSnapshot as parsePublicJwksSnapshot3}from"@eventsdk/sdk/contracts";import{constants as constants2}from"node:fs";import{lstat,open as open2,rename,unlink}from"node:fs/promises";import{dirname,isAbsolute as isAbsolute3}from"node:path";import{randomUUID}from"node:crypto";async function updatePublicConfiguration(file,update){if(!isAbsolute3(file)||file.includes("\0"))throw new EventSdkConfigurationError("FILE_REFERENCE_INVALID");const lock=`${file}.lock`,temporary=`${file}.${randomUUID()}.tmp`;let locked=false;try{const parent=await lstat(dirname(file));if(!parent.isDirectory()||(parent.mode&18)!==0)throw new Error("directory");const handle=await open2(lock,constants2.O_CREAT|constants2.O_EXCL|constants2.O_WRONLY|constants2.O_NOFOLLOW,384);locked=true;await handle.close();const previous=await existing(file);const output=update(previous);if(Buffer.byteLength(output)>65536)throw new Error("size");const next=await open2(temporary,constants2.O_CREAT|constants2.O_EXCL|constants2.O_WRONLY|constants2.O_NOFOLLOW,384);try{await next.writeFile(output,"utf8");await next.sync()}finally{await next.close()}await rename(temporary,file);const directory=await open2(dirname(file),constants2.O_RDONLY);try{await directory.sync()}finally{await directory.close()}}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("PUBLIC_SNAPSHOT_UPDATE_FAILED")}finally{await unlink(temporary).catch(()=>void 0);if(locked)await unlink(lock).catch(()=>void 0)}}async function existing(file){try{await lstat(file)}catch(error){if(error.code==="ENOENT")return null;throw error}return readConfigurationFile(file)}import{parsePublicJwksSnapshot as parsePublicJwksSnapshot2}from"@eventsdk/sdk/contracts";import{validatePublicOriginKeys as validatePublicOriginKeys2}from"@eventsdk/sdk/identity-verifier";function snapshotValue(issuer,jwksUri,jwks,now,ttlMs){if(!Number.isFinite(now)||!Number.isSafeInteger(ttlMs)||ttlMs<6e4||ttlMs>864e5)throw new EventSdkConfigurationError("SNAPSHOT_LIFETIME_INVALID");try{const value=parsePublicJwksSnapshot2({schemaVersion:1,issuer,jwksUri,fetchedAt:new Date(now).toISOString(),validUntil:new Date(now+ttlMs).toISOString(),jwks});validatePublicOriginKeys2(value.jwks);return value}catch{throw new EventSdkConfigurationError("PUBLIC_JWKS_INVALID")}}async function exportOwnPublicSnapshot(file,options={}){const{profile}=await loadNodeConnectionProfile(file);const config=profile.identity.signer;if(!config)throw new EventSdkConfigurationError("SIGNER_NOT_CONFIGURED");const pem=await readConfigurationFile(config.privateKeyFile,true);let key;try{const privateKey=createPrivateKey(pem);const publicKey=createPublicKey2(privateKey).export({format:"jwk"});const expected=config.algorithm==="EdDSA"?"OKP":config.algorithm==="ES256"?"EC":"RSA";if(!pem.startsWith("-----BEGIN PRIVATE KEY-----")||publicKey.kty!==expected||expected==="OKP"&&publicKey.crv!=="Ed25519"||expected==="EC"&&publicKey.crv!=="P-256"||expected==="RSA"&&(privateKey.asymmetricKeyDetails?.modulusLength??0)<2048)throw new Error("algorithm");key={...publicKey,kid:config.keyId,alg:config.algorithm,use:"sig"}}catch{throw new EventSdkConfigurationError("SIGNING_KEY_INVALID")}const now=options.now??Date.now(),ttl=options.ttlMs??36e5;await updatePublicConfiguration(config.publicSnapshotFile,previous=>{const keys=retained(previous,config.issuer,config.jwksUri);const same=keys.find(item=>item.kid===key.kid);if(same&&!samePublicKey(same,key))throw new EventSdkConfigurationError("KEY_ID_REUSE_REJECTED");const snapshot=snapshotValue(config.issuer,config.jwksUri,{keys:same?keys.map(item=>item===same?key:item):[...keys,key]},now,ttl);return JSON.stringify(snapshot,null,2)+"\n"});return{status:"PUBLIC_SNAPSHOT_EXPORTED",serviceRef:profile.serviceRef,keyId:config.keyId}}function samePublicKey(old,key){const fields=key.kty==="RSA"?["kty","n","e"]:key.kty==="EC"?["kty","crv","x","y"]:["kty","crv","x"];return fields.every(field=>old[field]===key[field])&&(old.alg===void 0||old.alg===key.alg)}function retained(previous,issuer,uri){if(!previous)return[];try{const parsed=parsePublicJwksSnapshot3(parseJsonStrict3(previous));if(parsed.issuer!==issuer||parsed.jwksUri!==uri)throw new Error("binding");return snapshotValue(issuer,uri,parsed.jwks,0,6e4).jwks.keys}catch{throw new EventSdkConfigurationError("EXISTING_PUBLIC_SNAPSHOT_INVALID")}}import{parseJsonStrict as parseJsonStrict4}from"@eventsdk/sdk/contracts";async function refreshTrustedPublicSnapshots(file,options={}){const{profile}=await loadNodeConnectionProfile(file);let refreshed=0;for(const binding of profile.identity.trustedSources){await refresh(binding,options);refreshed+=1}return{status:"TRUST_SNAPSHOTS_REFRESHED",serviceRef:profile.serviceRef,refreshed}}async function refresh(binding,options){try{const signal=AbortSignal.timeout(5e3);const response=await(options.fetch??globalThis.fetch)(binding.jwksUri,{method:"GET",redirect:"error",signal,headers:{accept:"application/json"}});if(!response.ok||response.redirected||!response.body)throw new Error("response");const reader=response.body.getReader(),chunks=[];let size=0;try{while(true){signal.throwIfAborted();const result=await reader.read();if(result.done)break;size+=result.value.byteLength;if(size>65536)throw new Error("size");chunks.push(result.value)}}finally{await reader.cancel().catch(()=>void 0);reader.releaseLock()}const raw=parseJsonStrict4(new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks)));const snapshot=snapshotValue(binding.issuer,binding.jwksUri,raw,options.now??Date.now(),options.ttlMs??36e5);await updatePublicConfiguration(binding.publicSnapshotFile,()=>JSON.stringify(snapshot,null,2)+"\n")}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("TRUST_REFRESH_FAILED")}}export{EventSdkConfigurationError,checkNodeConnectionConfiguration,exportOwnPublicSnapshot,loadNodeConnectionIdentity,loadNodeConnectionProfile,loadNodeLocalRecoveryOrigin,loadNodePublicIdentity,loadSigningIdentity as loadNodeSigningIdentity,loadPublicTrustSnapshot,refreshTrustedPublicSnapshots,validateNodeProfile};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{createHash}from"node:crypto";import{canonicalJson,parseJsonStrict}from"@eventsdk/sdk/contracts";import{constants}from"node:fs";import{open}from"node:fs/promises";import{isAbsolute}from"node:path";var EventSdkConfigurationError=class extends Error{constructor(code){super(code);this.code=code}code};async function readConfigurationFile(path,privateFile=false){if(!isAbsolute(path)||path.includes("\0"))throw new EventSdkConfigurationError("FILE_REFERENCE_INVALID");try{const file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);try{const stat=await file.stat();if(!stat.isFile()||stat.nlink!==1||stat.size<1||stat.size>65536||(stat.mode&18)!==0||privateFile&&(stat.mode&63)!==0)throw new EventSdkConfigurationError("FILE_PROTECTION_REQUIRED");const bytes=Buffer.alloc(65537);const read=await file.read(bytes,0,bytes.length,0);if(read.bytesRead>65536||read.bytesRead!==stat.size)throw new EventSdkConfigurationError("FILE_CHANGED");const after=await file.stat();if(after.size!==stat.size||after.mtimeMs!==stat.mtimeMs||after.ctimeMs!==stat.ctimeMs||after.nlink!==1)throw new EventSdkConfigurationError("FILE_CHANGED");return new TextDecoder("utf-8",{fatal:true}).decode(bytes.subarray(0,read.bytesRead))}finally{await file.close()}}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("FILE_UNAVAILABLE")}}import{isIP}from"node:net";import{isAbsolute as isAbsolute2}from"node:path";import{parseNodeConnectionProfile}from"@eventsdk/sdk/contracts";function validateNodeProfile(raw,expectedServiceRef){let profile;try{profile=parseNodeConnectionProfile(structuredClone(raw))}catch{throw new EventSdkConfigurationError("PROFILE_INVALID")}if(expectedServiceRef&&profile.serviceRef!==expectedServiceRef)fail("SERVICE_IDENTITY_MISMATCH");broker(profile);identity(profile);const source=new URL(profile.installationSource);if(source.username||source.password||source.search||source.hash)fail("INSTALLATION_SOURCE_INVALID");if(profile.publisher&&(!profile.identity.signer||!subject(profile.publisher.subject)||!entity(profile.publisher.expectedStream)))fail("PUBLISHER_IDENTITY_REQUIRED");if(profile.consumer&&(!profile.identity.trustedSources.length||!subject(profile.consumer.subject)||!entity(profile.consumer.stream)||!entity(profile.consumer.durableName)))fail("CONSUMER_IDENTITY_REQUIRED");if(!profile.publisher&&!profile.consumer)fail("CAPABILITY_ROLE_REQUIRED");routing(profile);return deepFreeze(profile)}function routing(profile){const publisher=profile.publisher;const audiences=profile.identity.signer?.audiences??[];if(publisher){const routes=publisher.audienceRoutes;if(!routes&&audiences.length!==1)fail("PUBLISHER_AUDIENCE_ROUTES_REQUIRED");if(routes&&(routes.length!==audiences.length||new Set(routes.map(r=>r.audience)).size!==routes.length||new Set(routes.map(r=>r.subject)).size!==routes.length||routes.some(r=>!audiences.includes(r.audience)||!subject(r.subject)||!entity(r.expectedStream))||routes[0]?.subject!==publisher.subject||routes[0]?.expectedStream!==publisher.expectedStream))fail("PUBLISHER_AUDIENCE_ROUTES_INVALID")}const consumer=profile.consumer;if(consumer){const bindings=[consumer,...consumer.additionalBindings??[]];if(new Set(bindings.map(b=>JSON.stringify([b.stream,b.durableName]))).size!==bindings.length||new Set(bindings.map(b=>b.subject)).size!==bindings.length||bindings.some(b=>!subject(b.subject)||!entity(b.stream)||!entity(b.durableName))||bindings.length*consumer.batchSize*profile.broker.timeoutMs>3e5)fail("CONSUMER_BINDINGS_INVALID")}}function broker(profile){const b=profile.broker,url=new URL(b.serverUrl);if(url.protocol!=="tls:"||url.username||url.password||url.search||url.hash||!["","/"].includes(url.pathname)||isIP(url.hostname.replace(/^\[|\]$/g,""))||url.hostname.length>253||url.hostname.split(".").some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))||b.expectedDnsName!==url.hostname)fail("BROKER_TLS_IDENTITY_REQUIRED");files([b.credentialsFile,...b.caFile?[b.caFile]:[]]);if(profile.consumer&&(profile.consumer.fetchExpiresMs>=b.timeoutMs||profile.consumer.batchSize*b.timeoutMs>3e5))fail("CONSUMER_BOUNDS_INVALID")}function identity(profile){const signer=profile.identity.signer;if(signer){https(signer.issuer);https(signer.jwksUri);files([signer.privateKeyFile,signer.publicSnapshotFile])}const seen=new Set;for(const trust of profile.identity.trustedSources){https(trust.issuer);https(trust.jwksUri);files([trust.publicSnapshotFile]);const key=JSON.stringify([trust.source,trust.serviceRef,trust.audience]);if(seen.has(key)||trust.audience!==profile.serviceRef)fail("TRUST_BINDING_AMBIGUOUS");seen.add(key)}}function https(value){const url=new URL(value);if(url.protocol!=="https:"||url.username||url.password||url.search||url.hash)fail("ISSUER_HTTPS_REQUIRED")}function files(values){if(values.some(path=>!isAbsolute2(path)||path.includes("\0")))fail("FILE_REFERENCE_INVALID")}function subject(value){return/^[^\s.*>]+(?:\.[^\s.*>]+)*$/u.test(value)}function entity(value){return/^[^\s.*>/\\]+$/u.test(value)}function fail(code){throw new EventSdkConfigurationError(code)}function deepFreeze(value){if(value&&typeof value==="object"&&!Object.isFrozen(value)){Object.freeze(value);Object.values(value).forEach(deepFreeze)}return value}async function loadNodeConnectionProfile(path,expectedServiceRef){const text=await readConfigurationFile(path);try{const profile=validateNodeProfile(parseJsonStrict(text),expectedServiceRef);return Object.freeze({profile,revision:`sha256:${createHash("sha256").update(canonicalJson(profile)).digest("hex")}`})}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("PROFILE_INVALID")}}import{createPublicKey}from"node:crypto";import{createEventOriginSigner}from"@eventsdk/sdk/identity-verifier";import{parsePublicJwksSnapshot,parseJsonStrict as parseJsonStrict2}from"@eventsdk/sdk/contracts";import{validatePublicOriginKeys}from"@eventsdk/sdk/identity-verifier";async function loadPublicTrustSnapshot(file,issuer,jwksUri,now=Date.now()){try{const snapshot=parsePublicJwksSnapshot(parseJsonStrict2(await readConfigurationFile(file)));const fetched=Date.parse(snapshot.fetchedAt),until=Date.parse(snapshot.validUntil);if(snapshot.issuer!==issuer||jwksUri&&snapshot.jwksUri!==jwksUri)fail2("TRUST_ISSUER_MISMATCH");const uri=new URL(snapshot.jwksUri);if(uri.protocol!=="https:"||uri.username||uri.password||uri.search||uri.hash)fail2("TRUST_URI_INVALID");if(!Number.isFinite(now)||fetched>now||now>=until||until<=fetched||until-fetched>864e5)fail2("TRUST_SNAPSHOT_EXPIRED");validatePublicOriginKeys(snapshot.jwks);return snapshot}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("TRUST_SNAPSHOT_INVALID")}}function fail2(code){throw new EventSdkConfigurationError(code)}async function loadSigningIdentity(profile,clock){const config=profile.identity.signer;if(!config)return null;const snapshot=await loadPublicTrustSnapshot(config.publicSnapshotFile,config.issuer,config.jwksUri,clock().getTime());const privateKeyPkcs8=await readConfigurationFile(config.privateKeyFile,true);assertKeyPair(config,snapshot.jwks.keys,privateKeyPkcs8);try{return await createEventOriginSigner({issuer:config.issuer,subject:config.subject,serviceRef:profile.serviceRef,source:profile.installationSource,keyId:config.keyId,algorithm:config.algorithm,privateKeyPkcs8,audiences:config.audiences,keysValidUntil:snapshot.validUntil,clock})}catch{throw new EventSdkConfigurationError("SIGNING_IDENTITY_INVALID")}}function assertKeyPair(config,keys,pem){try{const expected=keys.find(key=>key.kid===config.keyId);const actual=createPublicKey(pem).export({format:"jwk"});if(!expected||expected.alg!==void 0&&expected.alg!==config.algorithm||expected.kty!==actual.kty)throw new Error("mismatch");const fields=actual.kty==="RSA"?["n","e"]:actual.kty==="EC"?["crv","x","y"]:["crv","x"];if(fields.some(field=>expected[field]!==actual[field]))throw new Error("mismatch")}catch{throw new EventSdkConfigurationError("SIGNING_KEY_PAIR_MISMATCH")}}import{createEventOriginVerifier}from"@eventsdk/sdk/identity-verifier";async function loadTrustedSources(profile,clock){for(const binding of profile.identity.trustedSources)await verifier(binding,clock);return async(event,proof,signal)=>{signal.throwIfAborted();const selected=profile.identity.trustedSources.find(binding=>binding.source===event.source&&binding.serviceRef===event.serviceorigin&&binding.audience===event.audience);if(!selected)return{verified:false,reasonCode:"SOURCE_NOT_ENROLLED"};try{return await(await verifier(selected,clock)).verify(event,proof,signal)}catch(error){return{verified:false,reasonCode:signal.aborted?"CANCELLED":error instanceof EventSdkConfigurationError?error.code:"TRUST_SNAPSHOT_INVALID"}}}}async function verifier(binding,clock){const snapshot=await loadPublicTrustSnapshot(binding.publicSnapshotFile,binding.issuer,binding.jwksUri,clock().getTime());return createEventOriginVerifier({source:binding.source,serviceRef:binding.serviceRef,issuer:binding.issuer,subject:binding.subject,audience:binding.audience,algorithms:binding.algorithms,jwks:snapshot.jwks,keysValidUntil:snapshot.validUntil,clock})}async function loadNodeConnectionIdentity(loaded,clock=()=>new Date,inspectionOnly=false){if(loaded.profile.mode==="DISABLED"&&!inspectionOnly)throw new EventSdkConfigurationError("PROFILE_DISABLED");const config=loaded.profile.broker;await readConfigurationFile(config.credentialsFile,true);if(config.caFile)await readConfigurationFile(config.caFile);const signer=await loadSigningIdentity(loaded.profile,clock);const verifyOrigin=await loadTrustedSources(loaded.profile,clock);return Object.freeze({profile:loaded.profile,revision:loaded.revision,signer,verifyOrigin,async withBrokerCredentials(run){const credential=await readConfigurationFile(config.credentialsFile,true);const ca=config.caFile?await readConfigurationFile(config.caFile):void 0;return run({credential:new TextEncoder().encode(credential),...ca?{ca}:{}})}})}async function checkNodeConnectionConfiguration(file,expectedServiceRef,checkFiles=false){if(!file)return{status:"DISABLED",configured:false,connected:false};try{const loaded=await loadNodeConnectionProfile(file,expectedServiceRef),p=loaded.profile;if(checkFiles)await loadNodeConnectionIdentity(loaded,()=>new Date,true);return{status:p.mode==="DISABLED"?"DISABLED":checkFiles?"ASSETS_VALID":"PROFILE_VALID",configured:true,connected:false,serviceRef:p.serviceRef,mode:p.mode,revision:loaded.revision,roles:{publisher:p.publisher!==null,consumer:p.consumer!==null},originProof:p.identity.originProof,trustedSources:p.identity.trustedSources.length,filesChecked:checkFiles}}catch(error){return{status:"INVALID",configured:true,connected:false,code:error instanceof EventSdkConfigurationError?error.code:"CONFIGURATION_UNAVAILABLE"}}}import{createPrivateKey,createPublicKey as createPublicKey2}from"node:crypto";import{parseJsonStrict as parseJsonStrict3,parsePublicJwksSnapshot as parsePublicJwksSnapshot3}from"@eventsdk/sdk/contracts";import{constants as constants2}from"node:fs";import{lstat,open as open2,rename,unlink}from"node:fs/promises";import{dirname,isAbsolute as isAbsolute3}from"node:path";import{randomUUID}from"node:crypto";async function updatePublicConfiguration(file,update){if(!isAbsolute3(file)||file.includes("\0"))throw new EventSdkConfigurationError("FILE_REFERENCE_INVALID");const lock=`${file}.lock`,temporary=`${file}.${randomUUID()}.tmp`;let locked=false;try{const parent=await lstat(dirname(file));if(!parent.isDirectory()||(parent.mode&18)!==0)throw new Error("directory");const handle=await open2(lock,constants2.O_CREAT|constants2.O_EXCL|constants2.O_WRONLY|constants2.O_NOFOLLOW,384);locked=true;await handle.close();const previous=await existing(file);const output=update(previous);if(Buffer.byteLength(output)>65536)throw new Error("size");const next=await open2(temporary,constants2.O_CREAT|constants2.O_EXCL|constants2.O_WRONLY|constants2.O_NOFOLLOW,384);try{await next.writeFile(output,"utf8");await next.sync()}finally{await next.close()}await rename(temporary,file);const directory=await open2(dirname(file),constants2.O_RDONLY);try{await directory.sync()}finally{await directory.close()}}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("PUBLIC_SNAPSHOT_UPDATE_FAILED")}finally{await unlink(temporary).catch(()=>void 0);if(locked)await unlink(lock).catch(()=>void 0)}}async function existing(file){try{await lstat(file)}catch(error){if(error.code==="ENOENT")return null;throw error}return readConfigurationFile(file)}import{parsePublicJwksSnapshot as parsePublicJwksSnapshot2}from"@eventsdk/sdk/contracts";import{validatePublicOriginKeys as validatePublicOriginKeys2}from"@eventsdk/sdk/identity-verifier";function snapshotValue(issuer,jwksUri,jwks,now,ttlMs){if(!Number.isFinite(now)||!Number.isSafeInteger(ttlMs)||ttlMs<6e4||ttlMs>864e5)throw new EventSdkConfigurationError("SNAPSHOT_LIFETIME_INVALID");try{const value=parsePublicJwksSnapshot2({schemaVersion:1,issuer,jwksUri,fetchedAt:new Date(now).toISOString(),validUntil:new Date(now+ttlMs).toISOString(),jwks});validatePublicOriginKeys2(value.jwks);return value}catch{throw new EventSdkConfigurationError("PUBLIC_JWKS_INVALID")}}async function exportOwnPublicSnapshot(file,options={}){const{profile}=await loadNodeConnectionProfile(file);const config=profile.identity.signer;if(!config)throw new EventSdkConfigurationError("SIGNER_NOT_CONFIGURED");const pem=await readConfigurationFile(config.privateKeyFile,true);let key;try{const privateKey=createPrivateKey(pem);const publicKey=createPublicKey2(privateKey).export({format:"jwk"});const expected=config.algorithm==="EdDSA"?"OKP":config.algorithm==="ES256"?"EC":"RSA";if(!pem.startsWith("-----BEGIN PRIVATE KEY-----")||publicKey.kty!==expected||expected==="OKP"&&publicKey.crv!=="Ed25519"||expected==="EC"&&publicKey.crv!=="P-256"||expected==="RSA"&&(privateKey.asymmetricKeyDetails?.modulusLength??0)<2048)throw new Error("algorithm");key={...publicKey,kid:config.keyId,alg:config.algorithm,use:"sig"}}catch{throw new EventSdkConfigurationError("SIGNING_KEY_INVALID")}const now=options.now??Date.now(),ttl=options.ttlMs??36e5;await updatePublicConfiguration(config.publicSnapshotFile,previous=>{const keys=retained(previous,config.issuer,config.jwksUri);const same=keys.find(item=>item.kid===key.kid);if(same&&!samePublicKey(same,key))throw new EventSdkConfigurationError("KEY_ID_REUSE_REJECTED");const snapshot=snapshotValue(config.issuer,config.jwksUri,{keys:same?keys.map(item=>item===same?key:item):[...keys,key]},now,ttl);return JSON.stringify(snapshot,null,2)+"\n"});return{status:"PUBLIC_SNAPSHOT_EXPORTED",serviceRef:profile.serviceRef,keyId:config.keyId}}function samePublicKey(old,key){const fields=key.kty==="RSA"?["kty","n","e"]:key.kty==="EC"?["kty","crv","x","y"]:["kty","crv","x"];return fields.every(field=>old[field]===key[field])&&(old.alg===void 0||old.alg===key.alg)}function retained(previous,issuer,uri){if(!previous)return[];try{const parsed=parsePublicJwksSnapshot3(parseJsonStrict3(previous));if(parsed.issuer!==issuer||parsed.jwksUri!==uri)throw new Error("binding");return snapshotValue(issuer,uri,parsed.jwks,0,6e4).jwks.keys}catch{throw new EventSdkConfigurationError("EXISTING_PUBLIC_SNAPSHOT_INVALID")}}import{parseJsonStrict as parseJsonStrict4}from"@eventsdk/sdk/contracts";async function refreshTrustedPublicSnapshots(file,options={}){const{profile}=await loadNodeConnectionProfile(file);let refreshed=0;for(const binding of profile.identity.trustedSources){await refresh(binding,options);refreshed+=1}return{status:"TRUST_SNAPSHOTS_REFRESHED",serviceRef:profile.serviceRef,refreshed}}async function refresh(binding,options){try{const signal=AbortSignal.timeout(5e3);const response=await(options.fetch??globalThis.fetch)(binding.jwksUri,{method:"GET",redirect:"error",signal,headers:{accept:"application/json"}});if(!response.ok||response.redirected||!response.body)throw new Error("response");const reader=response.body.getReader(),chunks=[];let size=0;try{while(true){signal.throwIfAborted();const result2=await reader.read();if(result2.done)break;size+=result2.value.byteLength;if(size>65536)throw new Error("size");chunks.push(result2.value)}}finally{await reader.cancel().catch(()=>void 0);reader.releaseLock()}const raw=parseJsonStrict4(new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks)));const snapshot=snapshotValue(binding.issuer,binding.jwksUri,raw,options.now??Date.now(),options.ttlMs??36e5);await updatePublicConfiguration(binding.publicSnapshotFile,()=>JSON.stringify(snapshot,null,2)+"\n")}catch(error){if(error instanceof EventSdkConfigurationError)throw error;throw new EventSdkConfigurationError("TRUST_REFRESH_FAILED")}}async function runConfigurationCheck(args){if(args.length===2&&args[1]&&["export-own-public","refresh-trust"].includes(args[0]??"")){try{return args[0]==="export-own-public"?await exportOwnPublicSnapshot(args[1]):await refreshTrustedPublicSnapshots(args[1])}catch(error){return{status:"INVALID",code:error instanceof EventSdkConfigurationError?error.code:"CONFIGURATION_OPERATION_FAILED"}}}if(args[0]!=="check"||!args[1]||args.slice(2).some(arg=>arg!=="--check-files")||args.length>3)return{status:"INVALID",code:"USAGE",usage:"eventsdk-config check <absolute-profile-file> [--check-files] | export-own-public <file> | refresh-trust <file>"};return checkNodeConnectionConfiguration(args[1],void 0,args.includes("--check-files"))}var result=await runConfigurationCheck(process.argv.slice(2));process.stdout.write(`${JSON.stringify(result)}
|
|
3
|
+
`);if(result.status==="INVALID")process.exitCode=1;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface TrustRefreshOptions {
|
|
2
|
+
now?: number;
|
|
3
|
+
ttlMs?: number;
|
|
4
|
+
fetch?: typeof globalThis.fetch;
|
|
5
|
+
}
|
|
6
|
+
export declare function refreshTrustedPublicSnapshots(file: string, options?: TrustRefreshOptions): Promise<{
|
|
7
|
+
status: "TRUST_SNAPSHOTS_REFRESHED";
|
|
8
|
+
serviceRef: string;
|
|
9
|
+
refreshed: number;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type NodeConnectionProfile } from '@eventsdk/sdk/contracts';
|
|
2
|
+
export interface LoadedNodeProfile {
|
|
3
|
+
readonly profile: NodeConnectionProfile;
|
|
4
|
+
readonly revision: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadNodeConnectionProfile(path: string, expectedServiceRef?: string): Promise<LoadedNodeProfile>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { openNodeWorkerSession, type NodeWorkerSession } from './session.js';
|
|
2
|
+
export { createNativeRouteScheduler, type NativeRouteFault } from './route-scheduler.js';
|
|
3
|
+
export { runNodeEventWorker, type NodeWorkerState, type NodeWorkerTask, type NodeWorkerOptions, } from './lifecycle.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{loadNodeConnectionProfile,loadNodeConnectionIdentity,loadNodeSigningIdentity}from"@eventsdk/sdk/node-config";import{openNatsDeliverySession,credsAuthenticator,nkeyAuthenticator}from"@eventsdk/sdk/transport-nats";import{CoreError}from"@eventsdk/sdk/core";import{EventSdkConfigurationError}from"@eventsdk/sdk/node-config";function createNativeRouteScheduler(options={}){const now=options.now??Date.now,retryMs=options.retryMs??3e4,capacity=options.capacity??1024;if(!Number.isSafeInteger(retryMs)||retryMs<100||retryMs>3e5||!Number.isSafeInteger(capacity)||capacity<1||capacity>1e4)throw new Error("ROUTE_SCHEDULER_BOUNDS_INVALID");const retries=new Map;return{async run(routeRef,work,signal){signal.throwIfAborted();if((retries.get(routeRef)??0)>now())return false;try{await work();retries.delete(routeRef);return true}catch(error){signal.throwIfAborted();const retryAt=now()+retryMs;retries.delete(routeRef);while(retries.size>=capacity){const oldest=retries.keys().next().value;if(oldest===void 0)break;retries.delete(oldest)}retries.set(routeRef,retryAt);const code=error instanceof CoreError||error instanceof EventSdkConfigurationError?error.code:"ROUTE_UNAVAILABLE";options.fault?.({routeRef,reasonCode:/^[A-Z][A-Z0-9_]{0,63}$/.test(code)?code:"ROUTE_UNAVAILABLE",retryAt});return false}},held(){return[...retries].map(([routeRef,retryAt])=>({routeRef,retryAt}))}}}function routedPublisher(transport,profile,originProof){const publisher=profile.publisher;if(!publisher)return null;const audiences=profile.identity.signer?.audiences??[];const routes=publisher.audienceRoutes??audiences.map(audience=>({audience,subject:publisher.subject,expectedStream:publisher.expectedStream}));const ports=new Map(routes.map(route=>[route.audience,transport.publisher({subject:route.subject,expectedStream:route.expectedStream,timeoutMs:profile.broker.timeoutMs,maxPayloadBytes:profile.broker.maxPayloadBytes,...originProof?{originProof}:{}})]));return{async publish(event,context){const port=ports.get(event.audience);if(!port)throw Error("PUBLISH_AUDIENCE_NOT_CONFIGURED");return port.publish(event,context)}}}async function routedConsumer(transport,profile,signal){const consumer=profile.consumer;if(!consumer)return null;const bindings=[consumer,...consumer.additionalBindings??[]];const faults=new Map;const scheduler=createNativeRouteScheduler({capacity:bindings.length,retryMs:1e3,fault:value=>faults.set(value.routeRef,value)});const ports=[];for(const binding of bindings)ports.push(await openBinding(transport,profile,consumer,binding,signal));return{async consumeBatch(accept,child){let total=0;for(const[index,port]of ports.entries()){child.throwIfAborted();const binding=bindings[index];if(!binding)throw Error("CONSUMER_BINDING_MISSING");const ref=`${binding.stream}/${binding.durableName}`;if(ports.length===1){total+=await port.consumeBatch(accept,child);continue}if(await scheduler.run(ref,async()=>{total+=await port.consumeBatch(accept,child)},child))faults.delete(ref)}return total},heldRoutes(){return[...faults.values()]}}}function openBinding(transport,profile,consumer,binding,signal){return transport.consumer(binding.stream,{consumerName:binding.durableName,expectedStream:binding.stream,expectedSubject:binding.subject,deploymentDomainRef:profile.broker.deploymentDomainRef,timeoutMs:profile.broker.timeoutMs,maxPayloadBytes:profile.broker.maxPayloadBytes,retryDelayMs:1e3,batchSize:consumer.batchSize,fetchExpiresMs:consumer.fetchExpiresMs},signal)}async function openNodeWorkerSession(file,serviceRef,signal){const loaded=await loadNodeConnectionProfile(file,serviceRef);const identity=await loadNodeConnectionIdentity(loaded);const profile=loaded.profile;const transport=await openTransport(identity,signal);try{const signer=async()=>{const current=await loadNodeSigningIdentity(profile,()=>new Date);if(!current)throw new Error("SIGNER_NOT_CONFIGURED");return current};const publisher=routedPublisher(transport,profile,async(event,child)=>(await signer()).sign(event,child));const consumer=await routedConsumer(transport,profile,signal);return{profile,revision:loaded.revision,publisher,consumer,verifyOrigin:identity.verifyOrigin,refreshOrigin:async event=>(await signer()).currentOrigin(event.audience),isClosed:transport.isClosed,close:transport.close}}catch(error){await transport.close();throw error}}async function openTransport(identity,signal){const broker=identity.profile.broker;return identity.withBrokerCredentials(({credential,ca})=>openNatsDeliverySession({serverUrl:broker.serverUrl,expectedDnsName:broker.expectedDnsName,timeoutMs:broker.timeoutMs,signal,...ca?{caPem:ca}:{},authenticator:broker.authentication==="NATS_CREDS"?credsAuthenticator(credential):nkeyAuthenticator(new TextEncoder().encode(new TextDecoder().decode(credential).trim()))}))}import{loadNodeConnectionProfile as loadNodeConnectionProfile2,EventSdkConfigurationError as EventSdkConfigurationError2}from"@eventsdk/sdk/node-config";async function runNodeEventWorker(options,signal){const interval=bounded(options.intervalMs??1e3,100,3e4),retry=bounded(options.reconnectMs??2e3,100,3e4);const state=async(status,revision)=>options.state?.({schemaVersion:1,status,connected:status==="CONNECTED",at:new Date().toISOString(),...revision?{revision}:{}});try{while(!signal.aborted){const config=await loadNodeConnectionProfile2(options.file,options.serviceRef);if(config.profile.mode==="DISABLED"){await state("DISABLED",config.revision);return}try{await connected(options,signal,interval,state)}catch{if(signal.aborted)break;await state("RECONNECTING");await pause(retry,signal)}}}finally{await state("STOPPED")}}async function connected(options,signal,interval,state){const session=await(options.open??openNodeWorkerSession)(options.file,options.serviceRef,signal);let task;let closing;const close=()=>closing??=session.close();const abort=()=>{void close()};signal.addEventListener("abort",abort,{once:true});try{task=await options.tasks(session);if(session.isClosed())throw new Error("CONNECTION_CLOSED");await state("CONNECTED",session.revision);let heartbeatAt=Date.now();const heartbeatMs=bounded(options.heartbeatMs??3e4,100,3e5);while(!signal.aborted&&!session.isClosed()){const current=await loadNodeConnectionProfile2(options.file,options.serviceRef);if(current.revision!==session.revision)throw new EventSdkConfigurationError2("CONFIGURATION_CHANGED");await task.tick(signal);if(!signal.aborted&&!session.isClosed()&&Date.now()-heartbeatAt>=heartbeatMs){await state("CONNECTED",session.revision);heartbeatAt=Date.now()}await pause(interval,signal)}if(!signal.aborted)throw new Error("CONNECTION_CLOSED")}finally{signal.removeEventListener("abort",abort);await close();await task?.close()}}function bounded(value,min,max){if(!Number.isSafeInteger(value)||value<min||value>max)throw new EventSdkConfigurationError2("WORKER_BOUNDS_INVALID");return value}async function pause(ms,signal){if(signal.aborted)return;await new Promise(resolve=>{const finish=()=>{clearTimeout(timer);signal.removeEventListener("abort",finish);resolve()};const timer=setTimeout(finish,ms);signal.addEventListener("abort",finish,{once:true})})}export{createNativeRouteScheduler,openNodeWorkerSession,runNodeEventWorker};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { openNodeWorkerSession, type NodeWorkerSession } from './session.js';
|
|
2
|
+
import type { NodeWorkerObservation } from '@eventsdk/sdk/contracts';
|
|
3
|
+
export type NodeWorkerState = NodeWorkerObservation;
|
|
4
|
+
export interface NodeWorkerTask {
|
|
5
|
+
tick(signal: AbortSignal): Promise<void>;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface NodeWorkerOptions {
|
|
9
|
+
file: string;
|
|
10
|
+
serviceRef: string;
|
|
11
|
+
intervalMs?: number;
|
|
12
|
+
reconnectMs?: number;
|
|
13
|
+
heartbeatMs?: number;
|
|
14
|
+
tasks(session: NodeWorkerSession): Promise<NodeWorkerTask>;
|
|
15
|
+
state?(state: NodeWorkerState): Promise<void> | void;
|
|
16
|
+
open?: typeof openNodeWorkerSession;
|
|
17
|
+
}
|
|
18
|
+
export declare function runNodeEventWorker(options: NodeWorkerOptions, signal: AbortSignal): Promise<void>;
|
|
19
|
+
export declare function pause(ms: number, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface NativeRouteFault {
|
|
2
|
+
routeRef: string;
|
|
3
|
+
reasonCode: string;
|
|
4
|
+
retryAt: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function createNativeRouteScheduler(options?: {
|
|
7
|
+
now?: () => number;
|
|
8
|
+
retryMs?: number;
|
|
9
|
+
capacity?: number;
|
|
10
|
+
fault?: (value: NativeRouteFault) => void;
|
|
11
|
+
}): {
|
|
12
|
+
run(routeRef: string, work: () => Promise<unknown>, signal: AbortSignal): Promise<boolean>;
|
|
13
|
+
held(): {
|
|
14
|
+
routeRef: string;
|
|
15
|
+
retryAt: number;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CloudEvent } from '@eventsdk/sdk/contracts';
|
|
2
|
+
export declare function openNodeWorkerSession(file: string, serviceRef: string, signal: AbortSignal): Promise<{
|
|
3
|
+
profile: import("@eventsdk/sdk/contracts").NodeConnectionProfile;
|
|
4
|
+
revision: string;
|
|
5
|
+
publisher: import("@eventsdk/sdk/core").PublishPort | null;
|
|
6
|
+
consumer: {
|
|
7
|
+
consumeBatch(accept: import("@eventsdk/sdk/transport-nats").AcceptDelivery, child: AbortSignal): Promise<number>;
|
|
8
|
+
heldRoutes(): import("./route-scheduler.js").NativeRouteFault[];
|
|
9
|
+
} | null;
|
|
10
|
+
verifyOrigin: (event: import("@eventsdk/sdk/contracts").CloudEvent, proof: string, signal: AbortSignal) => Promise<import("@eventsdk/sdk/identity-contracts").OriginVerification>;
|
|
11
|
+
refreshOrigin: (event: CloudEvent) => Promise<import("@eventsdk/sdk/identity-contracts").VerifiedOrigin>;
|
|
12
|
+
isClosed: () => boolean;
|
|
13
|
+
close: () => Promise<void>;
|
|
14
|
+
}>;
|
|
15
|
+
export type NodeWorkerSession = Awaited<ReturnType<typeof openNodeWorkerSession>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProvisioningPlan } from '../planning/plan.js';
|
|
2
|
+
import type { ApplyResult, DeploymentJournal, BrokerManagementPort, DeploymentAuthorization } from './ports.js';
|
|
3
|
+
export interface ApplyRequest {
|
|
4
|
+
readonly manifest: unknown;
|
|
5
|
+
readonly journal?: DeploymentJournal;
|
|
6
|
+
readonly plan: ProvisioningPlan;
|
|
7
|
+
readonly authorization: DeploymentAuthorization;
|
|
8
|
+
readonly broker: BrokerManagementPort;
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
export declare function applyProvisioning(input: ApplyRequest): Promise<ApplyResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ResourceConfig, ResourceSnapshot } from '../manifest/types.js';
|
|
2
|
+
export interface DeploymentAuthorization {
|
|
3
|
+
readonly role: 'deployment';
|
|
4
|
+
readonly subject: string;
|
|
5
|
+
readonly deploymentDomainRef: string;
|
|
6
|
+
readonly allowedResourceKeys: readonly string[];
|
|
7
|
+
readonly allowedSubjects: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ManagementLease {
|
|
10
|
+
readonly generation: string;
|
|
11
|
+
release(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface BrokerReadPort {
|
|
14
|
+
read(signal: AbortSignal): Promise<readonly ResourceSnapshot[]>;
|
|
15
|
+
}
|
|
16
|
+
export interface BrokerManagementPort extends BrokerReadPort {
|
|
17
|
+
read(signal: AbortSignal): Promise<readonly ResourceSnapshot[]>;
|
|
18
|
+
acquireLease(domain: string, signal: AbortSignal): Promise<ManagementLease>;
|
|
19
|
+
create(config: ResourceConfig, lease: ManagementLease, signal: AbortSignal): Promise<void>;
|
|
20
|
+
update(config: ResourceConfig, expectedDigest: string, lease: ManagementLease, signal: AbortSignal): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface OperationReceipt {
|
|
23
|
+
readonly resourceKey: string;
|
|
24
|
+
readonly action: string;
|
|
25
|
+
readonly outcome: 'VERIFIED' | 'UNKNOWN' | 'BLOCKED';
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
readonly observedDigest?: string;
|
|
28
|
+
readonly nativeConfigDigest?: string;
|
|
29
|
+
readonly journalPersisted?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ApplyResult {
|
|
32
|
+
readonly planRef: string;
|
|
33
|
+
readonly state: 'VERIFIED' | 'PARTIAL' | 'BLOCKED';
|
|
34
|
+
readonly receipts: readonly OperationReceipt[];
|
|
35
|
+
readonly leaseReleased?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface DeploymentJournal {
|
|
38
|
+
append(entry: {
|
|
39
|
+
readonly planRef: string;
|
|
40
|
+
readonly resourceKey: string;
|
|
41
|
+
readonly phase: 'INTENT' | 'RESULT';
|
|
42
|
+
readonly receipt?: OperationReceipt;
|
|
43
|
+
}, signal: AbortSignal): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProvisioningPlan } from '../planning/plan.js';
|
|
2
|
+
import type { BrokerReadPort } from './ports.js';
|
|
3
|
+
export declare function verifyProvisioning(manifest: unknown, broker: BrokerReadPort, signal: AbortSignal): Promise<{
|
|
4
|
+
verified: boolean;
|
|
5
|
+
plan: ProvisioningPlan;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function provisioningStatus(broker: BrokerReadPort, signal: AbortSignal): Promise<readonly import("../index.js").ResourceSnapshot[]>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CliArguments {
|
|
2
|
+
readonly command: 'validate' | 'plan' | 'apply' | 'verify' | 'status' | 'conformance';
|
|
3
|
+
readonly manifestPath?: string;
|
|
4
|
+
readonly snapshotPath?: string;
|
|
5
|
+
readonly adapterPath?: string;
|
|
6
|
+
readonly planPath?: string;
|
|
7
|
+
readonly timeoutMs: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseArguments(args: readonly string[]): CliArguments;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CapabilityManifest } from '../manifest/types.js';
|
|
2
|
+
import type { CliArguments } from './arguments.js';
|
|
3
|
+
export interface CliResult {
|
|
4
|
+
readonly value: unknown;
|
|
5
|
+
readonly exitCode: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function runLiveCommand(args: CliArguments, manifest: CapabilityManifest | undefined, signal: AbortSignal): Promise<CliResult>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var ProvisionerError=class extends Error{constructor(code,message){super(message);this.code=code;this.name="ProvisionerError"}code};var publicCodes=new Set(["CLI_USAGE","CLI_INPUT_ERROR","INPUT_TOO_LARGE","INVALID_MANIFEST","INVALID_PLAN","INVALID_SNAPSHOT","INVALID_STATUS","INVALID_ADAPTER_PATH","INVALID_ADAPTER_MODULE","INVALID_ADAPTER_SESSION","LIVE_ADAPTER_REQUIRED","READ_ONLY_SESSION","ROLE_DURABILITY_CONFLICT","NO_CAPABILITIES","CAPABILITY_CONFLICT","PLAN_DRIFT","UNAUTHORIZED_DEPLOYMENT","DOMAIN_MISMATCH","BROKER_READ_UNAVAILABLE","UNSUPPORTED_BROKER_CONFIG","INVALID_INSPECTION_OPTIONS","AMBIGUOUS_RESOURCE","APPLY_INVARIANT"]);function publicErrorCode(error){if(error instanceof ProvisionerError&&publicCodes.has(error.code))return error.code;if(error instanceof Error&&"code"in error&&["TIMEOUT","ABORTED"].includes(String(error.code))){return String(error.code)}return"CLI_OPERATION_FAILED"}import{isSupportedMessagingProfile}from"@eventsdk/sdk/contracts";function parseDurability(outbox,inbox){if(outbox!=="owner-domain-transaction"&&outbox!=="native-event-log"&&outbox!=="not-applicable"||inbox!=="owner-durable"&&inbox!=="not-applicable"){throw new ProvisionerError("INVALID_MANIFEST","Explicit supported durability values are required.")}return{outbox,inbox}}function checkRoleDurability(manifest){const produces=manifest.emits.length>0;const consumes=manifest.consumes.length>0;if(!produces&&!consumes){throw new ProvisionerError("NO_CAPABILITIES","At least one explicit producer or consumer capability is required.")}if(produces===(manifest.durability.outbox==="not-applicable")||consumes===(manifest.durability.inbox==="not-applicable")){throw new ProvisionerError("ROLE_DURABILITY_CONFLICT","Durability must match the explicitly declared roles.")}}function checkCapabilities(m){const emittedTypes=new Set;const emittedSubjects=new Set;for(const emit of m.emits){if(emittedTypes.has(emit.type)||emittedSubjects.has(emit.subject))invalid();emittedTypes.add(emit.type);emittedSubjects.add(emit.subject);const streams=m.resources.filter(r=>r.kind==="stream"&&r.subjects.includes(emit.subject));if(streams.length!==1)invalid()}const consumerRefs=new Set;for(const consume of m.consumes){if(consumerRefs.has(consume.consumerRef))invalid();consumerRefs.add(consume.consumerRef);const resources=m.resources.filter(r=>r.kind==="consumer"&&r.name===consume.consumerRef);if(resources.length!==1||resources[0]?.subjects.length!==1||resources[0]?.subjects[0]!==consume.subject)invalid()}for(const resource2 of m.resources){if(resource2.kind==="stream"&&resource2.subjects.some(s=>!emittedSubjects.has(s)))invalid();if(resource2.kind==="consumer"&&!consumerRefs.has(resource2.name))invalid()}}function invalid(){throw new ProvisionerError("CAPABILITY_CONFLICT","Capabilities require explicit, unique resource/subject mappings.")}var token=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;var subject=/^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$/;function object(value,keys){if(!value||typeof value!=="object"||Array.isArray(value)){throw new ProvisionerError("INVALID_MANIFEST","Expected an object.")}const record2=value;if(Object.keys(record2).some(key=>!keys.includes(key))){throw new ProvisionerError("INVALID_MANIFEST","Unknown or sensitive fields are not allowed.")}return record2}function string(value,pattern=token){if(typeof value!=="string"||!pattern.test(value)){throw new ProvisionerError("INVALID_MANIFEST","Invalid identifier.")}return value}function integer(value,min,max=Number.MAX_SAFE_INTEGER){if(!Number.isSafeInteger(value)||typeof value!=="number"||value<min||value>max){throw new ProvisionerError("INVALID_MANIFEST","Invalid bounded integer.")}return value}function list(value){if(!Array.isArray(value)||value.length>1e3){throw new ProvisionerError("INVALID_MANIFEST","Expected a bounded array.")}return value}function parseResource(value){const r=object(value,["kind","name","owner","deploymentDomainRef","subjects","retentionSeconds","replicas","maxBytes","stream","ackWaitSeconds","maxAckPending"]);if(r.kind!=="stream"&&r.kind!=="consumer"){throw new ProvisionerError("INVALID_MANIFEST","Unsupported resource kind.")}const subjects=list(r.subjects).map(v=>string(v,subject));if(!subjects.length||new Set(subjects).size!==subjects.length){throw new ProvisionerError("INVALID_MANIFEST","Subjects must be nonempty and unique.")}const base={name:string(r.name),owner:string(r.owner),deploymentDomainRef:string(r.deploymentDomainRef),subjects};if(r.kind==="stream"){if(r.stream!==void 0||r.ackWaitSeconds!==void 0||r.maxAckPending!==void 0){throw new ProvisionerError("INVALID_MANIFEST","Consumer fields on stream.")}return{...base,kind:"stream",retentionSeconds:integer(r.retentionSeconds,1),replicas:integer(r.replicas,1,5),maxBytes:integer(r.maxBytes,1)}}if(["retentionSeconds","replicas","maxBytes"].some(key=>key in r))throw new ProvisionerError("INVALID_MANIFEST","Stream capacity fields on consumer.");return{...base,kind:"consumer",stream:string(r.stream),ackWaitSeconds:integer(r.ackWaitSeconds,1),maxAckPending:integer(r.maxAckPending,1)}}function parseManifest(value){const m=object(value,["messagingProfile","service","emits","consumes","transport","durability","resources"]);const t=object(m.transport,["kind","deploymentDomainRef"]);const d=object(m.durability,["outbox","inbox"]);if(t.kind!=="nats-jetstream"){throw new ProvisionerError("INVALID_MANIFEST","Unsupported transport.")}if(!isSupportedMessagingProfile(m.messagingProfile))throw new ProvisionerError("INVALID_MANIFEST","Unsupported messaging profile.");const manifest={messagingProfile:m.messagingProfile,service:string(m.service),emits:list(m.emits).map(v=>{const x=object(v,["type","subject"]);return{type:string(x.type,subject),subject:string(x.subject,subject)}}),consumes:list(m.consumes).map(parseConsume),transport:{kind:"nats-jetstream",deploymentDomainRef:string(t.deploymentDomainRef)},durability:parseDurability(d.outbox,d.inbox),resources:list(m.resources).map(parseResource)};checkOwnership(manifest);checkCapabilities(manifest);checkRoleDurability(manifest);return manifest}function parseConsume(value){const c=object(value,["fromService","type","consumerRef","subject"]);return{fromService:string(c.fromService),type:string(c.type,subject),consumerRef:string(c.consumerRef),subject:string(c.subject,subject)}}function checkOwnership(m){const ids=new Set;for(const resource2 of m.resources){const id=resource2.kind+":"+(resource2.kind==="consumer"?resource2.stream:"")+":"+resource2.name;if(ids.has(id)||resource2.owner!==m.service||resource2.deploymentDomainRef!==m.transport.deploymentDomainRef){throw new ProvisionerError("INVALID_MANIFEST","Duplicate resource or ownership mismatch.")}ids.add(id)}}import{createHash}from"node:crypto";function canonical(value){if(value===null||typeof value!=="object"){const encoded=JSON.stringify(value);if(encoded===void 0)throw new TypeError("Undefined is not canonical JSON.");return encoded}if(Array.isArray(value))return"["+value.map(canonical).join(",")+"]";const record2=value;return"{"+Object.keys(record2).sort().map(key=>JSON.stringify(key)+":"+canonical(record2[key])).join(",")+"}"}function digest(value){return createHash("sha256").update(canonical(value)).digest("hex")}function resourceKey(r){return r.kind+":"+(r.kind==="consumer"?r.stream:"")+":"+r.name}function configurationDigest(observed){return digest(observed.map(s=>({config:parseResource(s.config),nativeConfigDigest:s.nativeConfigDigest??null})).sort((a,b)=>resourceKey(a.config).localeCompare(resourceKey(b.config))))}function planProvisioning(input,observed){const manifest=parseManifest(input);const configs=new Map;for(const snapshot of observed){const config=parseResource(snapshot.config);const key=resourceKey(config);if(configs.has(key))throw new ProvisionerError("AMBIGUOUS_RESOURCE","Duplicate observed resource.");configs.set(key,config)}const operations=[...manifest.resources].sort((a,b)=>Number(a.kind==="consumer")-Number(b.kind==="consumer")).map(r=>classify(r,configs.get(resourceKey(r))));for(const operation2 of operations){const r=operation2.resource;if(r.kind!=="consumer"||operation2.action!=="CREATE")continue;const parent=configs.get("stream::"+r.stream)??operations.find(o=>o.resource.kind==="stream"&&o.resource.name===r.stream&&o.action==="CREATE")?.resource;if(!parent||parent.kind!=="stream"||parent.deploymentDomainRef!==r.deploymentDomainRef||r.subjects.some(subject2=>!parent.subjects.includes(subject2))){operations[operations.indexOf(operation2)]={...operation2,action:"BLOCKED",reason:"PARENT_DEPENDENCY_UNPROVEN"}}}const body={manifestDigest:digest(manifest),observedConfigDigest:configurationDigest(observed),operations};return{...body,planRef:digest(body)}}function classify(desired,current){const beforeDigest=current?digest(current):null;const base={resource:desired,beforeDigest};if(!current)return{...base,action:"CREATE",reason:"RESOURCE_ABSENT"};if(digest(desired)===beforeDigest)return{...base,action:"NOOP",reason:"MATCH"};const blocked=unsafeChange(current,desired);return blocked?{...base,action:"BLOCKED",reason:blocked}:{...base,action:"SAFE_UPDATE",reason:"MONOTONIC_CAPACITY_UPDATE"}}function unsafeChange(a,b){if(a.owner!==b.owner||a.deploymentDomainRef!==b.deploymentDomainRef)return"OWNER_CONFLICT";if(a.kind!==b.kind||digest(a.subjects)!==digest(b.subjects))return"ROUTING_CHANGE";if(a.kind==="stream"&&b.kind==="stream"){if(b.retentionSeconds<a.retentionSeconds||b.replicas<a.replicas||b.maxBytes<a.maxBytes)return"DURABILITY_REDUCTION"}if(a.kind==="consumer"&&b.kind==="consumer"){if(a.stream!==b.stream)return"ROUTING_CHANGE";if(a.ackWaitSeconds!==b.ackWaitSeconds)return"DELIVERY_POLICY_CHANGE";if(b.maxAckPending<a.maxAckPending)return"CAPACITY_REDUCTION"}return void 0}function rolePolicyCases(producer){const stream=producer.resources[0];if(!stream)throw new Error("Conformance fixture requires a producer stream.");const consumer=consumerFixture(producer,stream);return roleCases(producer,consumer)}function consumerFixture(producer,stream){return{...producer,emits:[],consumes:[{fromService:"upstream",type:"fixture.created.v1",subject:"fixture.created.v1",consumerRef:"worker"}],resources:[{owner:stream.owner,deploymentDomainRef:stream.deploymentDomainRef,subjects:stream.subjects,kind:"consumer",name:"worker",stream:"existing-upstream",ackWaitSeconds:30,maxAckPending:10}],durability:{outbox:"not-applicable",inbox:"owner-durable"}}}function roleCases(producer,consumer){return[{name:"producer-without-inbox",passed:parseManifest(producer).durability.inbox==="not-applicable"},{name:"consumer-without-outbox-or-stream-create",passed:planProvisioning(consumer,[]).operations.every(o=>o.resource.kind==="consumer")},{name:"reject-empty-roles",passed:rejects(()=>parseManifest({...producer,emits:[],resources:[],durability:{outbox:"not-applicable",inbox:"not-applicable"}}))},{name:"reject-role-durability-mismatch",passed:rejects(()=>parseManifest({...producer,durability:{outbox:"owner-domain-transaction",inbox:"owner-durable"}}))}]}function rejects(run){try{run();return false}catch{return true}}function provisionerConformance(){const resource2={kind:"stream",name:"fixture",owner:"fixture",deploymentDomainRef:"isolated",subjects:["fixture.created.v1"],retentionSeconds:60,replicas:1,maxBytes:1024};const manifest={messagingProfile:"eventsdk/1.0",service:"fixture",emits:[{type:"fixture.created.v1",subject:"fixture.created.v1"}],consumes:[],transport:{kind:"nats-jetstream",deploymentDomainRef:"isolated"},durability:{outbox:"owner-domain-transaction",inbox:"not-applicable"},resources:[resource2]};const tests=[...rolePolicyCases(parseManifest(manifest)),{name:"create-absent",passed:planProvisioning(manifest,[]).operations[0]?.action==="CREATE"},{name:"repeat-noop",passed:planProvisioning(manifest,[{config:resource2}]).operations[0]?.action==="NOOP"},{name:"block-downgrade",passed:planProvisioning(manifest,[{config:{...resource2,replicas:3}}]).operations[0]?.action==="BLOCKED"},{name:"reject-secret-fields",passed:rejects2(()=>parseManifest({...manifest,token:"fixture"}))}];return{suite:"provisioner-offline-policy/1.0",scope:"OFFLINE_POLICY_ONLY",passed:tests.every(t=>t.passed),tests}}function rejects2(run){try{run();return false}catch{return true}}function parseArguments(args){const command=args[0];if(!["validate","plan","apply","verify","status","conformance"].includes(command??""))usage();const options=new Map;const positional=[];for(let index=1;index<args.length;index++){const value=args[index];if(!value)usage();if(!value.startsWith("--")){positional.push(value);continue}if(!["--adapter","--plan","--timeout-ms"].includes(value)||options.has(value))usage();const argument=args[++index];if(!argument||argument.startsWith("--"))usage();options.set(value,argument)}const timeoutMs=Number(options.get("--timeout-ms")??"10000");if(!Number.isSafeInteger(timeoutMs)||timeoutMs<10||timeoutMs>12e4)usage();const adapterPath=options.get("--adapter");const planPath=options.get("--plan");if(planPath&&command!=="apply")usage();if(adapterPath&&["validate","conformance"].includes(command??""))usage();const expected=command==="conformance"||command==="status"&&adapterPath?0:command==="status"||command==="validate"||command==="apply"||adapterPath?1:2;if(positional.length!==expected)usage();if(command==="apply"&&adapterPath&&!planPath)usage();return{command,timeoutMs,...adapterPath===void 0?{}:{adapterPath},...planPath===void 0?{}:{planPath},...positional[0]===void 0?{}:command==="status"?{snapshotPath:positional[0]}:{manifestPath:positional[0]},...positional[1]===void 0?{}:{snapshotPath:positional[1]}}}function usage(){throw new ProvisionerError("CLI_USAGE","Invalid or ambiguous command arguments.")}import{open,stat}from"node:fs/promises";import{parseJsonStrict}from"@eventsdk/sdk/contracts";async function readCliJson(path){if(!path)throw new ProvisionerError("CLI_USAGE","A JSON file path is required.");if(!(await stat(path)).isFile()){throw new ProvisionerError("CLI_INPUT_ERROR","Expected a regular JSON file.")}const file=await open(path,"r");try{if(!(await file.stat()).isFile())throw new ProvisionerError("CLI_INPUT_ERROR","Expected a regular JSON file.");const limit=1048576;const bytes=new Uint8Array(limit+1);let used=0;while(used<=limit){const chunk=await file.read(bytes,used,bytes.length-used,null);if(chunk.bytesRead===0)break;used+=chunk.bytesRead;if(used>limit)throw new ProvisionerError("INPUT_TOO_LARGE","Input exceeds 1 MiB.")}return parseJsonStrict(bytes.subarray(0,used))}finally{await file.close()}}import{boundedOperation as boundedOperation2}from"@eventsdk/sdk/core";async function applyProvisioning(input){const manifest=parseManifest(structuredClone(input.manifest));const request={...input,manifest,plan:structuredClone(input.plan),authorization:structuredClone(input.authorization)};authorize(request,manifest.transport.deploymentDomainRef);request.signal.throwIfAborted();const lease=await request.broker.acquireLease(manifest.transport.deploymentDomainRef,request.signal);let result;let failure;let failed=false;try{const observed=await request.broker.read(request.signal);const actual=planProvisioning(manifest,observed);if(digest(actual)!==digest(request.plan)){throw new ProvisionerError("PLAN_DRIFT","Manifest, configuration or plan has changed.")}result=actual.operations.some(o=>o.action==="BLOCKED")?{planRef:actual.planRef,state:"BLOCKED",receipts:[]}:await execute({...request,plan:actual},lease,observed)}catch(error){failed=true;failure=error}let leaseReleased=true;try{await lease.release()}catch{leaseReleased=false}if(failed)throw failure;if(!result)throw new ProvisionerError("APPLY_INVARIANT","No apply result.");return{...result,leaseReleased}}function authorize(request,domain){const a=request.authorization;const manifest=parseManifest(request.manifest);if(a.role!=="deployment"||!a.subject||a.deploymentDomainRef!==domain||manifest.resources.some(r=>!a.allowedResourceKeys.includes(resourceKey(r))||r.subjects.some(s=>!a.allowedSubjects.includes(s)))){throw new ProvisionerError("UNAUTHORIZED_DEPLOYMENT","Explicit deployment authority is required.")}}async function execute(r,lease,observed){const receipts=[];let expected=configurationDigest(observed);for(const operation2 of r.plan.operations){let before;try{r.signal.throwIfAborted();before=await r.broker.read(r.signal)}catch{receipts.push({resourceKey:resourceKey(operation2.resource),action:operation2.action,outcome:"BLOCKED",reason:r.signal.aborted?"CANCELLED_BEFORE_WRITE":"READ_UNAVAILABLE"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}if(configurationDigest(before)!==expected){receipts.push({resourceKey:resourceKey(operation2.resource),action:operation2.action,outcome:"BLOCKED",reason:"PLAN_DRIFT"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}const receipt=await journaledExecute(r,lease,operation2);receipts.push(receipt);if(receipt.outcome!=="VERIFIED")return{planRef:r.plan.planRef,state:"PARTIAL",receipts};if(operation2.action==="NOOP")continue;const desired=before.filter(s=>resourceKey(s.config)!==resourceKey(operation2.resource));expected=configurationDigest([...desired,{config:operation2.resource,...receipt.nativeConfigDigest===void 0?{}:{nativeConfigDigest:receipt.nativeConfigDigest}}])}return verifyFinalConfiguration(r,expected,receipts)}async function verifyFinalConfiguration(r,expected,receipts){try{r.signal.throwIfAborted();if(configurationDigest(await r.broker.read(r.signal))!==expected){receipts.push({resourceKey:"plan",action:"VERIFY",outcome:"BLOCKED",reason:"PLAN_DRIFT"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}return{planRef:r.plan.planRef,state:"VERIFIED",receipts}}catch{receipts.push({resourceKey:"plan",action:"VERIFY",outcome:"UNKNOWN",reason:"FINAL_READ_UNAVAILABLE"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}}async function executeOne(r,lease,operation2){const base={resourceKey:resourceKey(operation2.resource),action:operation2.action};try{if(operation2.action==="CREATE")await r.broker.create(structuredClone(operation2.resource),lease,r.signal);if(operation2.action==="SAFE_UPDATE"){if(!operation2.beforeDigest)throw new ProvisionerError("INVALID_PLAN","Missing prior digest.");await r.broker.update(structuredClone(operation2.resource),operation2.beforeDigest,lease,r.signal)}const after=await r.broker.read(r.signal);const actual=after.find(s=>resourceKey(s.config)===base.resourceKey);if(actual&&digest(actual.config)===digest(operation2.resource)){return{...base,outcome:"VERIFIED",reason:"READBACK_MATCH",observedDigest:digest(actual.config),...actual.nativeConfigDigest===void 0?{}:{nativeConfigDigest:actual.nativeConfigDigest}}}return{...base,outcome:"UNKNOWN",reason:"READBACK_MISMATCH"}}catch{return{...base,outcome:"UNKNOWN",reason:"WRITE_OR_READBACK_UNCERTAIN"}}}async function journaledExecute(r,lease,operation2){const resource2=resourceKey(operation2.resource);if(!r.journal)return executeOne(r,lease,operation2);try{await r.journal.append({planRef:r.plan.planRef,resourceKey:resource2,phase:"INTENT"},r.signal)}catch{return{resourceKey:resource2,action:operation2.action,outcome:"BLOCKED",reason:"JOURNAL_INTENT_FAILED",journalPersisted:false}}const receipt=await executeOne(r,lease,operation2);try{await r.journal.append({planRef:r.plan.planRef,resourceKey:resource2,phase:"RESULT",receipt:structuredClone(receipt)},r.signal);return{...receipt,journalPersisted:true}}catch{return{...receipt,outcome:"UNKNOWN",reason:"JOURNAL_RESULT_FAILED",journalPersisted:false}}}async function verifyProvisioning(manifest,broker,signal){signal.throwIfAborted();const plan=planProvisioning(manifest,await broker.read(signal));return{verified:plan.operations.every(o=>o.action==="NOOP"),plan}}async function readReviewedPlan(path){const value=await readCliJson(path);const p=record(value,["manifestDigest","observedConfigDigest","planRef","operations"]);if(!Array.isArray(p.operations)||p.operations.length>1e3)invalid2();return{manifestDigest:hash(p.manifestDigest),observedConfigDigest:hash(p.observedConfigDigest),planRef:hash(p.planRef),operations:p.operations.map(operation)}}function operation(value){const o=record(value,["action","resource","beforeDigest","reason"]);if(!["NOOP","CREATE","SAFE_UPDATE","BLOCKED"].includes(String(o.action))||typeof o.reason!=="string"||!/^[A-Z0-9_]{1,128}$/.test(o.reason))invalid2();return{action:o.action,resource:parseResource(o.resource),beforeDigest:o.beforeDigest===null?null:hash(o.beforeDigest),reason:o.reason}}function record(value,keys){if(!value||typeof value!=="object"||Array.isArray(value)||Object.keys(value).some(key=>!keys.includes(key)))invalid2();return value}function hash(value){if(typeof value!=="string"||!/^[a-f0-9]{64}$/.test(value))invalid2();return value}function invalid2(){throw new ProvisionerError("INVALID_PLAN","Reviewed plan object required.")}import{realpath,stat as stat2}from"node:fs/promises";import{isAbsolute,extname}from"node:path";import{pathToFileURL}from"node:url";import{boundedOperation}from"@eventsdk/sdk/core";async function closeSession(session){try{await boundedOperation({timeoutMs:1e3},signal=>session.close(signal));return true}catch{return false}}async function loadSession(path,context,timeoutMs){if(!isAbsolute(path)||![".mjs",".js"].includes(extname(path))){throw new ProvisionerError("INVALID_ADAPTER_PATH","Explicit absolute local ESM path required.")}return boundedOperation({signal:context.signal,timeoutMs},async signal=>{const canonical2=await realpath(path);if(![".mjs",".js"].includes(extname(canonical2))||!(await stat2(canonical2)).isFile()){throw new ProvisionerError("INVALID_ADAPTER_PATH","Adapter must be a local regular ESM file.")}signal.throwIfAborted();const module=await import(pathToFileURL(canonical2).href);signal.throwIfAborted();const factory=field(module,"createProvisionerSession");if(typeof factory!=="function")throw new ProvisionerError("INVALID_ADAPTER_MODULE","Missing session factory.");const value=await factory({...context,signal,...context.manifest?{manifest:structuredClone(context.manifest)}:{}});if(signal.aborted){await cleanupUnvalidated(value);signal.throwIfAborted()}try{return validateSession(value)}catch(error){await cleanupUnvalidated(value);throw error}})}function field(value,key){return value&&typeof value==="object"?value[key]:void 0}function method(value,key){return typeof field(value,key)==="function"}function validateSession(value){const mode=field(value,"mode");const broker=field(value,"broker");if(field(value,"apiVersion")!=="eventsdk-provisioner-session/1"||!["read-only","deployment"].includes(String(mode))||!method(value,"close")||field(value,"status")!==void 0&&!method(value,"status")||!method(broker,"read")){throw new ProvisionerError("INVALID_ADAPTER_SESSION","Unsupported session contract.")}if(mode==="deployment"&&(!method(broker,"acquireLease")||!method(broker,"create")||!method(broker,"update")||!method(field(value,"journal"),"append")||!field(value,"authorization"))){throw new ProvisionerError("INVALID_ADAPTER_SESSION","Deployment requires authority, fencing port and journal.")}return value}async function cleanupUnvalidated(value){if(method(value,"close"))await closeSession(value)}function validateNatsStatus(value){const s=object2(value,["mode","natsDomain","accountIdentity","storageBytes","memoryBytes","maxStorageBytes","maxMemoryBytes","streamCount","consumerCount","resources"]);if(s.mode!=="LIVE_READ_ONLY"||s.accountIdentity!=="AUTHENTICATED_CONNECTION_NOT_EXPOSED_BY_JS_API"||!Array.isArray(s.resources)||s.resources.length>1e3)invalid3();return{mode:"LIVE_READ_ONLY",natsDomain:nullableName(s.natsDomain),accountIdentity:"AUTHENTICATED_CONNECTION_NOT_EXPOSED_BY_JS_API",storageBytes:integer2(s.storageBytes),memoryBytes:integer2(s.memoryBytes),maxStorageBytes:integer2(s.maxStorageBytes,-1),maxMemoryBytes:integer2(s.maxMemoryBytes,-1),streamCount:integer2(s.streamCount),consumerCount:integer2(s.consumerCount),resources:s.resources.map(resource)}}function resource(value){const r=object2(value,["kind","name","stream","owner","deploymentDomainRef","messages","bytes","firstSequence","lastSequence","pending","ackPending","ackFloor","nativeConfigDigest"]);if(r.kind!=="stream"&&r.kind!=="consumer"||typeof r.nativeConfigDigest!=="string"||!/^[a-f0-9]{64}$/.test(r.nativeConfigDigest))invalid3();return{kind:r.kind,name:name(r.name),stream:name(r.stream),owner:nullableName(r.owner),deploymentDomainRef:nullableName(r.deploymentDomainRef),messages:integer2(r.messages),bytes:integer2(r.bytes),firstSequence:integer2(r.firstSequence),lastSequence:integer2(r.lastSequence),pending:integer2(r.pending),ackPending:integer2(r.ackPending),ackFloor:r.ackFloor===null?null:integer2(r.ackFloor),nativeConfigDigest:r.nativeConfigDigest}}function object2(value,keys){if(!value||typeof value!=="object"||Array.isArray(value)||Object.keys(value).some(key=>!keys.includes(key)))invalid3();return value}function integer2(value,minimum=0){if(typeof value!=="number"||!Number.isSafeInteger(value)||value<minimum)invalid3();return value}function name(value){if(typeof value!=="string"||!/^[A-Za-z0-9_-]{1,128}$/.test(value))invalid3();return value}function nullableName(value){return value===null?null:name(value)}function invalid3(){throw new ProvisionerError("INVALID_STATUS","Unsupported live status shape.")}var JournalProgress=class{constructor(delegate,plan){this.plan=plan;this.journal={append:async(entry,signal)=>{const operation2=plan.operations.find(o=>resourceKey(o.resource)===entry.resourceKey);if(!operation2||entry.planRef!==plan.planRef)throw new Error("Journal entry outside plan.");this.receipts.set(entry.resourceKey,{resourceKey:entry.resourceKey,action:operation2.action,outcome:"UNKNOWN",reason:"JOURNAL_OR_APPLY_UNCONFIRMED",journalPersisted:false});const receipt=entry.receipt===void 0?void 0:structuredClone(entry.receipt);await delegate.append(structuredClone(entry),signal);if(entry.phase==="RESULT"&&receipt){this.receipts.set(entry.resourceKey,{...receipt,journalPersisted:true})}}}}plan;receipts=new Map;journal;interrupted(){return{planRef:this.plan.planRef,state:"PARTIAL",receipts:[...this.receipts.values()]}}};async function runLiveCommand(args,manifest,signal){if(!args.adapterPath)throw new ProvisionerError("LIVE_ADAPTER_REQUIRED","Explicit adapter required.");const reviewedPlan=args.command==="apply"?await readReviewedPlan(args.planPath):void 0;const command=args.command;const session=await loadSession(args.adapterPath,{command,signal,...manifest===void 0?{}:{manifest}},args.timeoutMs);let result;try{result=await dispatch(args,manifest,session,signal,reviewedPlan)}catch(error){await closeSession(session);throw error}const closed=await closeSession(session);return{value:{mode:"LIVE_ADAPTER",result:result.value,sessionClosed:closed},exitCode:closed?result.exitCode:4}}async function dispatch(args,manifest,session,signal,reviewedPlan){if(args.command==="apply"){if(session.mode!=="deployment")throw new ProvisionerError("READ_ONLY_SESSION","Apply requires a deployment session.");if(!manifest)throw new ProvisionerError("CLI_USAGE","Manifest required.");if(!reviewedPlan)throw new ProvisionerError("INVALID_PLAN","Reviewed plan required.");return runApply(args,manifest,session,signal,reviewedPlan)}return boundedOperation2({signal,timeoutMs:args.timeoutMs},async bounded=>{if(args.command==="status"){if(session.status)return{value:{coverage:"NATS_TELEMETRY",status:validateNatsStatus(await session.status(bounded))},exitCode:0};return{value:{coverage:"RESOURCES_ONLY",resources:safeSnapshots(await session.broker.read(bounded))},exitCode:0}}if(!manifest)throw new ProvisionerError("CLI_USAGE","Manifest required.");if(args.command==="verify"){const result=await verifyProvisioning(manifest,session.broker,bounded);return{value:result,exitCode:result.verified?0:3}}const plan=planProvisioning(manifest,await session.broker.read(bounded));return{value:{plan},exitCode:plan.operations.some(o=>o.action==="BLOCKED")?3:0}})}async function runApply(args,manifest,session,signal,plan){const progress=new JournalProgress(session.journal,plan);try{const result=await boundedOperation2({signal,timeoutMs:args.timeoutMs},bounded=>applyProvisioning({manifest,plan,broker:session.broker,authorization:session.authorization,journal:progress.journal,signal:bounded}));return{value:result,exitCode:result.state==="VERIFIED"&&result.leaseReleased!==false?0:4}}catch(error){if(isInterrupted(error))return{value:{...progress.interrupted(),interruption:"CANCELLED_OR_TIMEOUT"},exitCode:4};throw error}}function isInterrupted(error){return error instanceof Error&&"code"in error&&["ABORTED","TIMEOUT"].includes(String(error.code))}function safeSnapshots(snapshots2){if(snapshots2.length>1e3)throw new ProvisionerError("INVALID_SNAPSHOT","Too many resources.");return snapshots2.map(s=>{const config=parseResource(s.config);const counters={};for(const key of["messages","bytes","consumerPending"]){if(s[key]!==void 0){if(!Number.isSafeInteger(s[key])||s[key]<0)throw new ProvisionerError("INVALID_SNAPSHOT","Invalid resource counters.");counters[key]=s[key]}}return{config,...counters}})}function snapshots(value){if(!Array.isArray(value))throw new ProvisionerError("INVALID_SNAPSHOT","Expected configuration array.");return value.map(config=>({config:parseResource(config)}))}async function runProvisionerCli(args,signal){const options=parseArguments(args);if(options.command==="conformance"){const result=provisionerConformance();return{value:result,exitCode:result.passed?0:3}}if(options.command==="apply"&&!options.adapterPath){throw new ProvisionerError("LIVE_ADAPTER_REQUIRED","Explicit trusted deployment adapter required.")}const manifest=options.command==="status"?void 0:parseManifest(await readCliJson(options.manifestPath));if(options.adapterPath)return runLiveCommand(options,manifest,signal);if(options.command==="status"){return{value:{mode:"OFFLINE_SNAPSHOT",resources:snapshots(await readCliJson(options.snapshotPath))},exitCode:0}}if(options.command==="validate")return{value:{valid:true,manifest},exitCode:0};const plan=planProvisioning(manifest,snapshots(await readCliJson(options.snapshotPath)));const verified=plan.operations.every(o=>o.action==="NOOP");return{value:{mode:"OFFLINE_SNAPSHOT",verified,plan},exitCode:options.command==="verify"&&!verified||plan.operations.some(o=>o.action==="BLOCKED")?3:0}}var controller=new AbortController;var cancel=()=>controller.abort();process.once("SIGINT",cancel);process.once("SIGTERM",cancel);try{const result=await runProvisionerCli(process.argv.slice(2),controller.signal);process.stdout.write(JSON.stringify(result.value,null,2)+"\n");process.exitCode=result.exitCode}catch(error){const code=publicErrorCode(error);process.stderr.write(JSON.stringify({code})+"\n");process.exitCode=1}finally{process.off("SIGINT",cancel);process.off("SIGTERM",cancel)}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NatsInspectionStatus } from '../nats/types.js';
|
|
2
|
+
import type { CapabilityManifest } from '../manifest/types.js';
|
|
3
|
+
import type { BrokerManagementPort, BrokerReadPort, DeploymentAuthorization, DeploymentJournal } from '../application/ports.js';
|
|
4
|
+
export interface SessionContext {
|
|
5
|
+
readonly command: 'plan' | 'apply' | 'verify' | 'status';
|
|
6
|
+
readonly signal: AbortSignal;
|
|
7
|
+
readonly manifest?: CapabilityManifest;
|
|
8
|
+
}
|
|
9
|
+
interface SessionBase {
|
|
10
|
+
readonly apiVersion: 'eventsdk-provisioner-session/1';
|
|
11
|
+
readonly close: (signal: AbortSignal) => Promise<void>;
|
|
12
|
+
readonly status?: (signal: AbortSignal) => Promise<NatsInspectionStatus>;
|
|
13
|
+
}
|
|
14
|
+
export interface ReadOnlyProvisionerSession extends SessionBase {
|
|
15
|
+
readonly mode: 'read-only';
|
|
16
|
+
readonly broker: BrokerReadPort;
|
|
17
|
+
}
|
|
18
|
+
export interface DeploymentProvisionerSession extends SessionBase {
|
|
19
|
+
readonly mode: 'deployment';
|
|
20
|
+
readonly broker: BrokerManagementPort;
|
|
21
|
+
readonly authorization: DeploymentAuthorization;
|
|
22
|
+
readonly journal: DeploymentJournal;
|
|
23
|
+
}
|
|
24
|
+
export type ProvisionerSession = ReadOnlyProvisionerSession | DeploymentProvisionerSession;
|
|
25
|
+
export type SessionFactory = (context: SessionContext) => Promise<ProvisionerSession>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { ProvisionerError } from './manifest/types.js';
|
|
2
|
+
export type { CapabilityManifest, ResourceConfig, StreamResourceConfig, ConsumerResourceConfig, ResourceSnapshot, } from './manifest/types.js';
|
|
3
|
+
export { parseManifest, parseResource } from './manifest/validate.js';
|
|
4
|
+
export { digest } from './planning/digest.js';
|
|
5
|
+
export { planProvisioning, configurationDigest, resourceKey, desiredResources, } from './planning/plan.js';
|
|
6
|
+
export type { PlanOperation, ProvisioningPlan, OperationKind } from './planning/plan.js';
|
|
7
|
+
export { applyProvisioning } from './application/apply.js';
|
|
8
|
+
export type { ApplyRequest } from './application/apply.js';
|
|
9
|
+
export type { BrokerManagementPort, BrokerReadPort, DeploymentJournal, DeploymentAuthorization, ManagementLease, ApplyResult, OperationReceipt, } from './application/ports.js';
|
|
10
|
+
export { verifyProvisioning, provisioningStatus } from './application/verify.js';
|
|
11
|
+
export { provisionerConformance } from './conformance/policy.js';
|
|
12
|
+
export { NatsReadOnlyInspector } from './nats/inspector.js';
|
|
13
|
+
export type { NatsInspectionOptions, InspectResource, NatsInspectionStatus, NatsResourceStatus, } from './nats/types.js';
|
|
14
|
+
export type { ProvisionerSession, ReadOnlyProvisionerSession, DeploymentProvisionerSession, SessionContext, SessionFactory, } from './cli/session-types.js';
|
|
15
|
+
export { runProvisionerCli } from './cli/run.js';
|
|
16
|
+
export { NatsExclusiveWriter } from './nats/writer.js';
|
|
17
|
+
export type { NatsExclusiveAccess, NatsWriterOptions } from './nats/writer-types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var ProvisionerError=class extends Error{constructor(code,message){super(message);this.code=code;this.name="ProvisionerError"}code};import{isSupportedMessagingProfile}from"@eventsdk/sdk/contracts";function parseDurability(outbox,inbox){if(outbox!=="owner-domain-transaction"&&outbox!=="native-event-log"&&outbox!=="not-applicable"||inbox!=="owner-durable"&&inbox!=="not-applicable"){throw new ProvisionerError("INVALID_MANIFEST","Explicit supported durability values are required.")}return{outbox,inbox}}function checkRoleDurability(manifest){const produces=manifest.emits.length>0;const consumes=manifest.consumes.length>0;if(!produces&&!consumes){throw new ProvisionerError("NO_CAPABILITIES","At least one explicit producer or consumer capability is required.")}if(produces===(manifest.durability.outbox==="not-applicable")||consumes===(manifest.durability.inbox==="not-applicable")){throw new ProvisionerError("ROLE_DURABILITY_CONFLICT","Durability must match the explicitly declared roles.")}}function checkCapabilities(m){const emittedTypes=new Set;const emittedSubjects=new Set;for(const emit of m.emits){if(emittedTypes.has(emit.type)||emittedSubjects.has(emit.subject))invalid();emittedTypes.add(emit.type);emittedSubjects.add(emit.subject);const streams=m.resources.filter(r=>r.kind==="stream"&&r.subjects.includes(emit.subject));if(streams.length!==1)invalid()}const consumerRefs=new Set;for(const consume of m.consumes){if(consumerRefs.has(consume.consumerRef))invalid();consumerRefs.add(consume.consumerRef);const resources=m.resources.filter(r=>r.kind==="consumer"&&r.name===consume.consumerRef);if(resources.length!==1||resources[0]?.subjects.length!==1||resources[0]?.subjects[0]!==consume.subject)invalid()}for(const resource2 of m.resources){if(resource2.kind==="stream"&&resource2.subjects.some(s=>!emittedSubjects.has(s)))invalid();if(resource2.kind==="consumer"&&!consumerRefs.has(resource2.name))invalid()}}function invalid(){throw new ProvisionerError("CAPABILITY_CONFLICT","Capabilities require explicit, unique resource/subject mappings.")}var token=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;var subject=/^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$/;function object(value,keys){if(!value||typeof value!=="object"||Array.isArray(value)){throw new ProvisionerError("INVALID_MANIFEST","Expected an object.")}const record2=value;if(Object.keys(record2).some(key=>!keys.includes(key))){throw new ProvisionerError("INVALID_MANIFEST","Unknown or sensitive fields are not allowed.")}return record2}function string(value,pattern=token){if(typeof value!=="string"||!pattern.test(value)){throw new ProvisionerError("INVALID_MANIFEST","Invalid identifier.")}return value}function integer(value,min,max=Number.MAX_SAFE_INTEGER){if(!Number.isSafeInteger(value)||typeof value!=="number"||value<min||value>max){throw new ProvisionerError("INVALID_MANIFEST","Invalid bounded integer.")}return value}function list(value){if(!Array.isArray(value)||value.length>1e3){throw new ProvisionerError("INVALID_MANIFEST","Expected a bounded array.")}return value}function parseResource(value){const r=object(value,["kind","name","owner","deploymentDomainRef","subjects","retentionSeconds","replicas","maxBytes","stream","ackWaitSeconds","maxAckPending"]);if(r.kind!=="stream"&&r.kind!=="consumer"){throw new ProvisionerError("INVALID_MANIFEST","Unsupported resource kind.")}const subjects=list(r.subjects).map(v=>string(v,subject));if(!subjects.length||new Set(subjects).size!==subjects.length){throw new ProvisionerError("INVALID_MANIFEST","Subjects must be nonempty and unique.")}const base={name:string(r.name),owner:string(r.owner),deploymentDomainRef:string(r.deploymentDomainRef),subjects};if(r.kind==="stream"){if(r.stream!==void 0||r.ackWaitSeconds!==void 0||r.maxAckPending!==void 0){throw new ProvisionerError("INVALID_MANIFEST","Consumer fields on stream.")}return{...base,kind:"stream",retentionSeconds:integer(r.retentionSeconds,1),replicas:integer(r.replicas,1,5),maxBytes:integer(r.maxBytes,1)}}if(["retentionSeconds","replicas","maxBytes"].some(key=>key in r))throw new ProvisionerError("INVALID_MANIFEST","Stream capacity fields on consumer.");return{...base,kind:"consumer",stream:string(r.stream),ackWaitSeconds:integer(r.ackWaitSeconds,1),maxAckPending:integer(r.maxAckPending,1)}}function parseManifest(value){const m=object(value,["messagingProfile","service","emits","consumes","transport","durability","resources"]);const t=object(m.transport,["kind","deploymentDomainRef"]);const d=object(m.durability,["outbox","inbox"]);if(t.kind!=="nats-jetstream"){throw new ProvisionerError("INVALID_MANIFEST","Unsupported transport.")}if(!isSupportedMessagingProfile(m.messagingProfile))throw new ProvisionerError("INVALID_MANIFEST","Unsupported messaging profile.");const manifest={messagingProfile:m.messagingProfile,service:string(m.service),emits:list(m.emits).map(v=>{const x=object(v,["type","subject"]);return{type:string(x.type,subject),subject:string(x.subject,subject)}}),consumes:list(m.consumes).map(parseConsume),transport:{kind:"nats-jetstream",deploymentDomainRef:string(t.deploymentDomainRef)},durability:parseDurability(d.outbox,d.inbox),resources:list(m.resources).map(parseResource)};checkOwnership(manifest);checkCapabilities(manifest);checkRoleDurability(manifest);return manifest}function parseConsume(value){const c=object(value,["fromService","type","consumerRef","subject"]);return{fromService:string(c.fromService),type:string(c.type,subject),consumerRef:string(c.consumerRef),subject:string(c.subject,subject)}}function checkOwnership(m){const ids=new Set;for(const resource2 of m.resources){const id=resource2.kind+":"+(resource2.kind==="consumer"?resource2.stream:"")+":"+resource2.name;if(ids.has(id)||resource2.owner!==m.service||resource2.deploymentDomainRef!==m.transport.deploymentDomainRef){throw new ProvisionerError("INVALID_MANIFEST","Duplicate resource or ownership mismatch.")}ids.add(id)}}import{createHash}from"node:crypto";function canonical(value){if(value===null||typeof value!=="object"){const encoded=JSON.stringify(value);if(encoded===void 0)throw new TypeError("Undefined is not canonical JSON.");return encoded}if(Array.isArray(value))return"["+value.map(canonical).join(",")+"]";const record2=value;return"{"+Object.keys(record2).sort().map(key=>JSON.stringify(key)+":"+canonical(record2[key])).join(",")+"}"}function digest(value){return createHash("sha256").update(canonical(value)).digest("hex")}function resourceKey(r){return r.kind+":"+(r.kind==="consumer"?r.stream:"")+":"+r.name}function configurationDigest(observed){return digest(observed.map(s=>({config:parseResource(s.config),nativeConfigDigest:s.nativeConfigDigest??null})).sort((a,b)=>resourceKey(a.config).localeCompare(resourceKey(b.config))))}function planProvisioning(input,observed){const manifest=parseManifest(input);const configs=new Map;for(const snapshot of observed){const config=parseResource(snapshot.config);const key=resourceKey(config);if(configs.has(key))throw new ProvisionerError("AMBIGUOUS_RESOURCE","Duplicate observed resource.");configs.set(key,config)}const operations=[...manifest.resources].sort((a,b)=>Number(a.kind==="consumer")-Number(b.kind==="consumer")).map(r=>classify(r,configs.get(resourceKey(r))));for(const operation2 of operations){const r=operation2.resource;if(r.kind!=="consumer"||operation2.action!=="CREATE")continue;const parent=configs.get("stream::"+r.stream)??operations.find(o=>o.resource.kind==="stream"&&o.resource.name===r.stream&&o.action==="CREATE")?.resource;if(!parent||parent.kind!=="stream"||parent.deploymentDomainRef!==r.deploymentDomainRef||r.subjects.some(subject2=>!parent.subjects.includes(subject2))){operations[operations.indexOf(operation2)]={...operation2,action:"BLOCKED",reason:"PARENT_DEPENDENCY_UNPROVEN"}}}const body={manifestDigest:digest(manifest),observedConfigDigest:configurationDigest(observed),operations};return{...body,planRef:digest(body)}}function classify(desired,current){const beforeDigest=current?digest(current):null;const base={resource:desired,beforeDigest};if(!current)return{...base,action:"CREATE",reason:"RESOURCE_ABSENT"};if(digest(desired)===beforeDigest)return{...base,action:"NOOP",reason:"MATCH"};const blocked=unsafeChange(current,desired);return blocked?{...base,action:"BLOCKED",reason:blocked}:{...base,action:"SAFE_UPDATE",reason:"MONOTONIC_CAPACITY_UPDATE"}}function unsafeChange(a,b){if(a.owner!==b.owner||a.deploymentDomainRef!==b.deploymentDomainRef)return"OWNER_CONFLICT";if(a.kind!==b.kind||digest(a.subjects)!==digest(b.subjects))return"ROUTING_CHANGE";if(a.kind==="stream"&&b.kind==="stream"){if(b.retentionSeconds<a.retentionSeconds||b.replicas<a.replicas||b.maxBytes<a.maxBytes)return"DURABILITY_REDUCTION"}if(a.kind==="consumer"&&b.kind==="consumer"){if(a.stream!==b.stream)return"ROUTING_CHANGE";if(a.ackWaitSeconds!==b.ackWaitSeconds)return"DELIVERY_POLICY_CHANGE";if(b.maxAckPending<a.maxAckPending)return"CAPACITY_REDUCTION"}return void 0}function desiredResources(manifest){return manifest.resources}async function applyProvisioning(input){const manifest=parseManifest(structuredClone(input.manifest));const request={...input,manifest,plan:structuredClone(input.plan),authorization:structuredClone(input.authorization)};authorize(request,manifest.transport.deploymentDomainRef);request.signal.throwIfAborted();const lease=await request.broker.acquireLease(manifest.transport.deploymentDomainRef,request.signal);let result;let failure;let failed=false;try{const observed=await request.broker.read(request.signal);const actual=planProvisioning(manifest,observed);if(digest(actual)!==digest(request.plan)){throw new ProvisionerError("PLAN_DRIFT","Manifest, configuration or plan has changed.")}result=actual.operations.some(o=>o.action==="BLOCKED")?{planRef:actual.planRef,state:"BLOCKED",receipts:[]}:await execute({...request,plan:actual},lease,observed)}catch(error){failed=true;failure=error}let leaseReleased=true;try{await lease.release()}catch{leaseReleased=false}if(failed)throw failure;if(!result)throw new ProvisionerError("APPLY_INVARIANT","No apply result.");return{...result,leaseReleased}}function authorize(request,domain){const a=request.authorization;const manifest=parseManifest(request.manifest);if(a.role!=="deployment"||!a.subject||a.deploymentDomainRef!==domain||manifest.resources.some(r=>!a.allowedResourceKeys.includes(resourceKey(r))||r.subjects.some(s=>!a.allowedSubjects.includes(s)))){throw new ProvisionerError("UNAUTHORIZED_DEPLOYMENT","Explicit deployment authority is required.")}}async function execute(r,lease,observed){const receipts=[];let expected=configurationDigest(observed);for(const operation2 of r.plan.operations){let before;try{r.signal.throwIfAborted();before=await r.broker.read(r.signal)}catch{receipts.push({resourceKey:resourceKey(operation2.resource),action:operation2.action,outcome:"BLOCKED",reason:r.signal.aborted?"CANCELLED_BEFORE_WRITE":"READ_UNAVAILABLE"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}if(configurationDigest(before)!==expected){receipts.push({resourceKey:resourceKey(operation2.resource),action:operation2.action,outcome:"BLOCKED",reason:"PLAN_DRIFT"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}const receipt=await journaledExecute(r,lease,operation2);receipts.push(receipt);if(receipt.outcome!=="VERIFIED")return{planRef:r.plan.planRef,state:"PARTIAL",receipts};if(operation2.action==="NOOP")continue;const desired=before.filter(s=>resourceKey(s.config)!==resourceKey(operation2.resource));expected=configurationDigest([...desired,{config:operation2.resource,...receipt.nativeConfigDigest===void 0?{}:{nativeConfigDigest:receipt.nativeConfigDigest}}])}return verifyFinalConfiguration(r,expected,receipts)}async function verifyFinalConfiguration(r,expected,receipts){try{r.signal.throwIfAborted();if(configurationDigest(await r.broker.read(r.signal))!==expected){receipts.push({resourceKey:"plan",action:"VERIFY",outcome:"BLOCKED",reason:"PLAN_DRIFT"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}return{planRef:r.plan.planRef,state:"VERIFIED",receipts}}catch{receipts.push({resourceKey:"plan",action:"VERIFY",outcome:"UNKNOWN",reason:"FINAL_READ_UNAVAILABLE"});return{planRef:r.plan.planRef,state:"PARTIAL",receipts}}}async function executeOne(r,lease,operation2){const base={resourceKey:resourceKey(operation2.resource),action:operation2.action};try{if(operation2.action==="CREATE")await r.broker.create(structuredClone(operation2.resource),lease,r.signal);if(operation2.action==="SAFE_UPDATE"){if(!operation2.beforeDigest)throw new ProvisionerError("INVALID_PLAN","Missing prior digest.");await r.broker.update(structuredClone(operation2.resource),operation2.beforeDigest,lease,r.signal)}const after=await r.broker.read(r.signal);const actual=after.find(s=>resourceKey(s.config)===base.resourceKey);if(actual&&digest(actual.config)===digest(operation2.resource)){return{...base,outcome:"VERIFIED",reason:"READBACK_MATCH",observedDigest:digest(actual.config),...actual.nativeConfigDigest===void 0?{}:{nativeConfigDigest:actual.nativeConfigDigest}}}return{...base,outcome:"UNKNOWN",reason:"READBACK_MISMATCH"}}catch{return{...base,outcome:"UNKNOWN",reason:"WRITE_OR_READBACK_UNCERTAIN"}}}async function journaledExecute(r,lease,operation2){const resource2=resourceKey(operation2.resource);if(!r.journal)return executeOne(r,lease,operation2);try{await r.journal.append({planRef:r.plan.planRef,resourceKey:resource2,phase:"INTENT"},r.signal)}catch{return{resourceKey:resource2,action:operation2.action,outcome:"BLOCKED",reason:"JOURNAL_INTENT_FAILED",journalPersisted:false}}const receipt=await executeOne(r,lease,operation2);try{await r.journal.append({planRef:r.plan.planRef,resourceKey:resource2,phase:"RESULT",receipt:structuredClone(receipt)},r.signal);return{...receipt,journalPersisted:true}}catch{return{...receipt,outcome:"UNKNOWN",reason:"JOURNAL_RESULT_FAILED",journalPersisted:false}}}async function verifyProvisioning(manifest,broker,signal){signal.throwIfAborted();const plan=planProvisioning(manifest,await broker.read(signal));return{verified:plan.operations.every(o=>o.action==="NOOP"),plan}}async function provisioningStatus(broker,signal){signal.throwIfAborted();return broker.read(signal)}function rolePolicyCases(producer){const stream=producer.resources[0];if(!stream)throw new Error("Conformance fixture requires a producer stream.");const consumer=consumerFixture(producer,stream);return roleCases(producer,consumer)}function consumerFixture(producer,stream){return{...producer,emits:[],consumes:[{fromService:"upstream",type:"fixture.created.v1",subject:"fixture.created.v1",consumerRef:"worker"}],resources:[{owner:stream.owner,deploymentDomainRef:stream.deploymentDomainRef,subjects:stream.subjects,kind:"consumer",name:"worker",stream:"existing-upstream",ackWaitSeconds:30,maxAckPending:10}],durability:{outbox:"not-applicable",inbox:"owner-durable"}}}function roleCases(producer,consumer){return[{name:"producer-without-inbox",passed:parseManifest(producer).durability.inbox==="not-applicable"},{name:"consumer-without-outbox-or-stream-create",passed:planProvisioning(consumer,[]).operations.every(o=>o.resource.kind==="consumer")},{name:"reject-empty-roles",passed:rejects(()=>parseManifest({...producer,emits:[],resources:[],durability:{outbox:"not-applicable",inbox:"not-applicable"}}))},{name:"reject-role-durability-mismatch",passed:rejects(()=>parseManifest({...producer,durability:{outbox:"owner-domain-transaction",inbox:"owner-durable"}}))}]}function rejects(run){try{run();return false}catch{return true}}function provisionerConformance(){const resource2={kind:"stream",name:"fixture",owner:"fixture",deploymentDomainRef:"isolated",subjects:["fixture.created.v1"],retentionSeconds:60,replicas:1,maxBytes:1024};const manifest={messagingProfile:"eventsdk/1.0",service:"fixture",emits:[{type:"fixture.created.v1",subject:"fixture.created.v1"}],consumes:[],transport:{kind:"nats-jetstream",deploymentDomainRef:"isolated"},durability:{outbox:"owner-domain-transaction",inbox:"not-applicable"},resources:[resource2]};const tests=[...rolePolicyCases(parseManifest(manifest)),{name:"create-absent",passed:planProvisioning(manifest,[]).operations[0]?.action==="CREATE"},{name:"repeat-noop",passed:planProvisioning(manifest,[{config:resource2}]).operations[0]?.action==="NOOP"},{name:"block-downgrade",passed:planProvisioning(manifest,[{config:{...resource2,replicas:3}}]).operations[0]?.action==="BLOCKED"},{name:"reject-secret-fields",passed:rejects2(()=>parseManifest({...manifest,token:"fixture"}))}];return{suite:"provisioner-offline-policy/1.0",scope:"OFFLINE_POLICY_ONLY",passed:tests.every(t=>t.passed),tests}}function rejects2(run){try{run();return false}catch{return true}}import{JetStreamApiError}from"@nats-io/jetstream";import{boundedOperation}from"@eventsdk/sdk/core";function streamSnapshot(info,domain){const c=info.config;if(c.mirror||c.sources?.length||c.subject_transform||c.republish||c.retention!=="limits"||c.storage!=="file"){throw new ProvisionerError("UNSUPPORTED_BROKER_CONFIG","Unsupported replication or retention policy.")}const config=parseResource({kind:"stream",name:c.name,owner:c.metadata?.["eventsdk.owner"],deploymentDomainRef:c.metadata?.["eventsdk.deployment-domain"],subjects:c.subjects,retentionSeconds:c.max_age/1e9,replicas:c.num_replicas,maxBytes:c.max_bytes});assertDomain(config,domain);return{config,nativeConfigDigest:digest(c),messages:info.state.messages,bytes:info.state.bytes}}function consumerSnapshot(info,stream,domain){const c=info.config;if(c.ack_policy!=="explicit"||c.deliver_subject||!c.durable_name||c.deliver_policy!=="all"||c.num_replicas!==void 0&&c.num_replicas!==0||c.mem_storage===true||Boolean(c.backoff?.length))throw new ProvisionerError("UNSUPPORTED_BROKER_CONFIG","Unsupported consumer policy.");streamSnapshot(stream,domain);const config=parseResource({kind:"consumer",name:info.name,stream:info.stream_name,owner:c.metadata?.["eventsdk.owner"],deploymentDomainRef:c.metadata?.["eventsdk.deployment-domain"],subjects:c.filter_subjects??(c.filter_subject?[c.filter_subject]:[]),ackWaitSeconds:(c.ack_wait??0)/1e9,maxAckPending:c.max_ack_pending});assertDomain(config,domain);return{config,nativeConfigDigest:digest(c),messages:stream.state.messages,bytes:stream.state.bytes,consumerPending:info.num_pending}}function assertDomain(config,domain){if(config.deploymentDomainRef!==domain)throw new ProvisionerError("DOMAIN_MISMATCH","Unexpected deployment domain metadata.")}function resourceStatus(stream,consumer){const c=consumer?.config??stream.config;return{kind:consumer?"consumer":"stream",name:consumer?.name??stream.config.name,stream:stream.config.name,owner:c.metadata?.["eventsdk.owner"]??null,deploymentDomainRef:c.metadata?.["eventsdk.deployment-domain"]??null,messages:stream.state.messages,bytes:stream.state.bytes,firstSequence:stream.state.first_seq,lastSequence:stream.state.last_seq,pending:consumer?.num_pending??0,ackPending:consumer?.num_ack_pending??0,ackFloor:consumer?.ack_floor.stream_seq??null,nativeConfigDigest:digest(c)}}var NatsReadOnlyInspector=class{constructor(manager,options){this.manager=manager;this.options=structuredClone(options);if(!options.deploymentDomainRef||new Set(options.resources.map(r=>r.kind+":"+(r.stream??"")+":"+r.name)).size!==options.resources.length||options.resources.length>1e3||options.resources.some(r=>!["stream","consumer"].includes(r.kind)||r.kind==="stream"&&r.stream!==void 0||!/^[A-Za-z0-9_-]+$/.test(r.name)||r.kind==="consumer"&&(!r.stream||!/^[A-Za-z0-9_-]+$/.test(r.stream)))){throw new ProvisionerError("INVALID_INSPECTION_OPTIONS","Invalid explicit resource selection.")}}manager;options;async read(signal){return boundedOperation({signal,timeoutMs:this.options.timeoutMs},async bounded=>{await this.account(bounded);const snapshots2=new Map;for(const ref of this.options.resources){bounded.throwIfAborted();const info=await this.resource(ref);if(!info)continue;const parent=streamSnapshot(info.stream,this.options.deploymentDomainRef);const key="stream::"+parent.config.name;const previous=snapshots2.get(key);if(previous&&previous.nativeConfigDigest!==parent.nativeConfigDigest)throw new ProvisionerError("PLAN_DRIFT","Parent changed during inspection.");snapshots2.set(key,parent);if(info.consumer)snapshots2.set("consumer:"+info.consumer.stream_name+":"+info.consumer.name,consumerSnapshot(info.consumer,info.stream,this.options.deploymentDomainRef))}return[...snapshots2.values()]})}async status(signal){return boundedOperation({signal,timeoutMs:this.options.timeoutMs},async bounded=>{const account=await this.account(bounded);const resources=[];for(const ref of this.options.resources){bounded.throwIfAborted();const info=await this.resource(ref);if(info&&(ref.kind==="stream"||info.consumer))resources.push(resourceStatus(info.stream,info.consumer))}return{mode:"LIVE_READ_ONLY",natsDomain:account.domain??null,accountIdentity:"AUTHENTICATED_CONNECTION_NOT_EXPOSED_BY_JS_API",storageBytes:account.storage,memoryBytes:account.memory,maxStorageBytes:account.limits.max_storage,maxMemoryBytes:account.limits.max_memory,streamCount:account.streams,consumerCount:account.consumers,resources}})}async account(signal){signal.throwIfAborted();const result=await this.manager.getAccountInfo().catch(()=>{throw new ProvisionerError("BROKER_READ_UNAVAILABLE","Authenticated account read failed.")});if((result.domain??null)!==this.options.expectedNatsDomain){throw new ProvisionerError("DOMAIN_MISMATCH","Unexpected authenticated NATS domain.")}return result}async resource(ref){try{const stream=await this.manager.streams.info(ref.stream??ref.name);if(ref.kind==="stream")return{stream};const consumer=await this.manager.consumers.info(ref.stream??"",ref.name).catch(error=>{if(error instanceof JetStreamApiError&&error.status===404&&error.code===10014)return void 0;throw error});return consumer?{stream,consumer}:{stream}}catch(error){if(error instanceof JetStreamApiError&&error.status===404&&(error.code===10059||error.code===10014))return null;throw new ProvisionerError("BROKER_READ_UNAVAILABLE","Authoritative broker read failed.")}}};function parseArguments(args){const command=args[0];if(!["validate","plan","apply","verify","status","conformance"].includes(command??""))usage();const options=new Map;const positional=[];for(let index=1;index<args.length;index++){const value=args[index];if(!value)usage();if(!value.startsWith("--")){positional.push(value);continue}if(!["--adapter","--plan","--timeout-ms"].includes(value)||options.has(value))usage();const argument=args[++index];if(!argument||argument.startsWith("--"))usage();options.set(value,argument)}const timeoutMs=Number(options.get("--timeout-ms")??"10000");if(!Number.isSafeInteger(timeoutMs)||timeoutMs<10||timeoutMs>12e4)usage();const adapterPath=options.get("--adapter");const planPath=options.get("--plan");if(planPath&&command!=="apply")usage();if(adapterPath&&["validate","conformance"].includes(command??""))usage();const expected=command==="conformance"||command==="status"&&adapterPath?0:command==="status"||command==="validate"||command==="apply"||adapterPath?1:2;if(positional.length!==expected)usage();if(command==="apply"&&adapterPath&&!planPath)usage();return{command,timeoutMs,...adapterPath===void 0?{}:{adapterPath},...planPath===void 0?{}:{planPath},...positional[0]===void 0?{}:command==="status"?{snapshotPath:positional[0]}:{manifestPath:positional[0]},...positional[1]===void 0?{}:{snapshotPath:positional[1]}}}function usage(){throw new ProvisionerError("CLI_USAGE","Invalid or ambiguous command arguments.")}import{open,stat}from"node:fs/promises";import{parseJsonStrict}from"@eventsdk/sdk/contracts";async function readCliJson(path){if(!path)throw new ProvisionerError("CLI_USAGE","A JSON file path is required.");if(!(await stat(path)).isFile()){throw new ProvisionerError("CLI_INPUT_ERROR","Expected a regular JSON file.")}const file=await open(path,"r");try{if(!(await file.stat()).isFile())throw new ProvisionerError("CLI_INPUT_ERROR","Expected a regular JSON file.");const limit=1048576;const bytes=new Uint8Array(limit+1);let used=0;while(used<=limit){const chunk=await file.read(bytes,used,bytes.length-used,null);if(chunk.bytesRead===0)break;used+=chunk.bytesRead;if(used>limit)throw new ProvisionerError("INPUT_TOO_LARGE","Input exceeds 1 MiB.")}return parseJsonStrict(bytes.subarray(0,used))}finally{await file.close()}}import{boundedOperation as boundedOperation3}from"@eventsdk/sdk/core";async function readReviewedPlan(path){const value=await readCliJson(path);const p=record(value,["manifestDigest","observedConfigDigest","planRef","operations"]);if(!Array.isArray(p.operations)||p.operations.length>1e3)invalid2();return{manifestDigest:hash(p.manifestDigest),observedConfigDigest:hash(p.observedConfigDigest),planRef:hash(p.planRef),operations:p.operations.map(operation)}}function operation(value){const o=record(value,["action","resource","beforeDigest","reason"]);if(!["NOOP","CREATE","SAFE_UPDATE","BLOCKED"].includes(String(o.action))||typeof o.reason!=="string"||!/^[A-Z0-9_]{1,128}$/.test(o.reason))invalid2();return{action:o.action,resource:parseResource(o.resource),beforeDigest:o.beforeDigest===null?null:hash(o.beforeDigest),reason:o.reason}}function record(value,keys){if(!value||typeof value!=="object"||Array.isArray(value)||Object.keys(value).some(key=>!keys.includes(key)))invalid2();return value}function hash(value){if(typeof value!=="string"||!/^[a-f0-9]{64}$/.test(value))invalid2();return value}function invalid2(){throw new ProvisionerError("INVALID_PLAN","Reviewed plan object required.")}import{realpath,stat as stat2}from"node:fs/promises";import{isAbsolute,extname}from"node:path";import{pathToFileURL}from"node:url";import{boundedOperation as boundedOperation2}from"@eventsdk/sdk/core";async function closeSession(session){try{await boundedOperation2({timeoutMs:1e3},signal=>session.close(signal));return true}catch{return false}}async function loadSession(path,context,timeoutMs){if(!isAbsolute(path)||![".mjs",".js"].includes(extname(path))){throw new ProvisionerError("INVALID_ADAPTER_PATH","Explicit absolute local ESM path required.")}return boundedOperation2({signal:context.signal,timeoutMs},async signal=>{const canonical2=await realpath(path);if(![".mjs",".js"].includes(extname(canonical2))||!(await stat2(canonical2)).isFile()){throw new ProvisionerError("INVALID_ADAPTER_PATH","Adapter must be a local regular ESM file.")}signal.throwIfAborted();const module=await import(pathToFileURL(canonical2).href);signal.throwIfAborted();const factory=field(module,"createProvisionerSession");if(typeof factory!=="function")throw new ProvisionerError("INVALID_ADAPTER_MODULE","Missing session factory.");const value=await factory({...context,signal,...context.manifest?{manifest:structuredClone(context.manifest)}:{}});if(signal.aborted){await cleanupUnvalidated(value);signal.throwIfAborted()}try{return validateSession(value)}catch(error){await cleanupUnvalidated(value);throw error}})}function field(value,key){return value&&typeof value==="object"?value[key]:void 0}function method(value,key){return typeof field(value,key)==="function"}function validateSession(value){const mode=field(value,"mode");const broker=field(value,"broker");if(field(value,"apiVersion")!=="eventsdk-provisioner-session/1"||!["read-only","deployment"].includes(String(mode))||!method(value,"close")||field(value,"status")!==void 0&&!method(value,"status")||!method(broker,"read")){throw new ProvisionerError("INVALID_ADAPTER_SESSION","Unsupported session contract.")}if(mode==="deployment"&&(!method(broker,"acquireLease")||!method(broker,"create")||!method(broker,"update")||!method(field(value,"journal"),"append")||!field(value,"authorization"))){throw new ProvisionerError("INVALID_ADAPTER_SESSION","Deployment requires authority, fencing port and journal.")}return value}async function cleanupUnvalidated(value){if(method(value,"close"))await closeSession(value)}function validateNatsStatus(value){const s=object2(value,["mode","natsDomain","accountIdentity","storageBytes","memoryBytes","maxStorageBytes","maxMemoryBytes","streamCount","consumerCount","resources"]);if(s.mode!=="LIVE_READ_ONLY"||s.accountIdentity!=="AUTHENTICATED_CONNECTION_NOT_EXPOSED_BY_JS_API"||!Array.isArray(s.resources)||s.resources.length>1e3)invalid3();return{mode:"LIVE_READ_ONLY",natsDomain:nullableName(s.natsDomain),accountIdentity:"AUTHENTICATED_CONNECTION_NOT_EXPOSED_BY_JS_API",storageBytes:integer2(s.storageBytes),memoryBytes:integer2(s.memoryBytes),maxStorageBytes:integer2(s.maxStorageBytes,-1),maxMemoryBytes:integer2(s.maxMemoryBytes,-1),streamCount:integer2(s.streamCount),consumerCount:integer2(s.consumerCount),resources:s.resources.map(resource)}}function resource(value){const r=object2(value,["kind","name","stream","owner","deploymentDomainRef","messages","bytes","firstSequence","lastSequence","pending","ackPending","ackFloor","nativeConfigDigest"]);if(r.kind!=="stream"&&r.kind!=="consumer"||typeof r.nativeConfigDigest!=="string"||!/^[a-f0-9]{64}$/.test(r.nativeConfigDigest))invalid3();return{kind:r.kind,name:name(r.name),stream:name(r.stream),owner:nullableName(r.owner),deploymentDomainRef:nullableName(r.deploymentDomainRef),messages:integer2(r.messages),bytes:integer2(r.bytes),firstSequence:integer2(r.firstSequence),lastSequence:integer2(r.lastSequence),pending:integer2(r.pending),ackPending:integer2(r.ackPending),ackFloor:r.ackFloor===null?null:integer2(r.ackFloor),nativeConfigDigest:r.nativeConfigDigest}}function object2(value,keys){if(!value||typeof value!=="object"||Array.isArray(value)||Object.keys(value).some(key=>!keys.includes(key)))invalid3();return value}function integer2(value,minimum=0){if(typeof value!=="number"||!Number.isSafeInteger(value)||value<minimum)invalid3();return value}function name(value){if(typeof value!=="string"||!/^[A-Za-z0-9_-]{1,128}$/.test(value))invalid3();return value}function nullableName(value){return value===null?null:name(value)}function invalid3(){throw new ProvisionerError("INVALID_STATUS","Unsupported live status shape.")}var JournalProgress=class{constructor(delegate,plan){this.plan=plan;this.journal={append:async(entry,signal)=>{const operation2=plan.operations.find(o=>resourceKey(o.resource)===entry.resourceKey);if(!operation2||entry.planRef!==plan.planRef)throw new Error("Journal entry outside plan.");this.receipts.set(entry.resourceKey,{resourceKey:entry.resourceKey,action:operation2.action,outcome:"UNKNOWN",reason:"JOURNAL_OR_APPLY_UNCONFIRMED",journalPersisted:false});const receipt=entry.receipt===void 0?void 0:structuredClone(entry.receipt);await delegate.append(structuredClone(entry),signal);if(entry.phase==="RESULT"&&receipt){this.receipts.set(entry.resourceKey,{...receipt,journalPersisted:true})}}}}plan;receipts=new Map;journal;interrupted(){return{planRef:this.plan.planRef,state:"PARTIAL",receipts:[...this.receipts.values()]}}};async function runLiveCommand(args,manifest,signal){if(!args.adapterPath)throw new ProvisionerError("LIVE_ADAPTER_REQUIRED","Explicit adapter required.");const reviewedPlan=args.command==="apply"?await readReviewedPlan(args.planPath):void 0;const command=args.command;const session=await loadSession(args.adapterPath,{command,signal,...manifest===void 0?{}:{manifest}},args.timeoutMs);let result;try{result=await dispatch(args,manifest,session,signal,reviewedPlan)}catch(error){await closeSession(session);throw error}const closed=await closeSession(session);return{value:{mode:"LIVE_ADAPTER",result:result.value,sessionClosed:closed},exitCode:closed?result.exitCode:4}}async function dispatch(args,manifest,session,signal,reviewedPlan){if(args.command==="apply"){if(session.mode!=="deployment")throw new ProvisionerError("READ_ONLY_SESSION","Apply requires a deployment session.");if(!manifest)throw new ProvisionerError("CLI_USAGE","Manifest required.");if(!reviewedPlan)throw new ProvisionerError("INVALID_PLAN","Reviewed plan required.");return runApply(args,manifest,session,signal,reviewedPlan)}return boundedOperation3({signal,timeoutMs:args.timeoutMs},async bounded=>{if(args.command==="status"){if(session.status)return{value:{coverage:"NATS_TELEMETRY",status:validateNatsStatus(await session.status(bounded))},exitCode:0};return{value:{coverage:"RESOURCES_ONLY",resources:safeSnapshots(await session.broker.read(bounded))},exitCode:0}}if(!manifest)throw new ProvisionerError("CLI_USAGE","Manifest required.");if(args.command==="verify"){const result=await verifyProvisioning(manifest,session.broker,bounded);return{value:result,exitCode:result.verified?0:3}}const plan=planProvisioning(manifest,await session.broker.read(bounded));return{value:{plan},exitCode:plan.operations.some(o=>o.action==="BLOCKED")?3:0}})}async function runApply(args,manifest,session,signal,plan){const progress=new JournalProgress(session.journal,plan);try{const result=await boundedOperation3({signal,timeoutMs:args.timeoutMs},bounded=>applyProvisioning({manifest,plan,broker:session.broker,authorization:session.authorization,journal:progress.journal,signal:bounded}));return{value:result,exitCode:result.state==="VERIFIED"&&result.leaseReleased!==false?0:4}}catch(error){if(isInterrupted(error))return{value:{...progress.interrupted(),interruption:"CANCELLED_OR_TIMEOUT"},exitCode:4};throw error}}function isInterrupted(error){return error instanceof Error&&"code"in error&&["ABORTED","TIMEOUT"].includes(String(error.code))}function safeSnapshots(snapshots2){if(snapshots2.length>1e3)throw new ProvisionerError("INVALID_SNAPSHOT","Too many resources.");return snapshots2.map(s=>{const config=parseResource(s.config);const counters={};for(const key of["messages","bytes","consumerPending"]){if(s[key]!==void 0){if(!Number.isSafeInteger(s[key])||s[key]<0)throw new ProvisionerError("INVALID_SNAPSHOT","Invalid resource counters.");counters[key]=s[key]}}return{config,...counters}})}function snapshots(value){if(!Array.isArray(value))throw new ProvisionerError("INVALID_SNAPSHOT","Expected configuration array.");return value.map(config=>({config:parseResource(config)}))}async function runProvisionerCli(args,signal){const options=parseArguments(args);if(options.command==="conformance"){const result=provisionerConformance();return{value:result,exitCode:result.passed?0:3}}if(options.command==="apply"&&!options.adapterPath){throw new ProvisionerError("LIVE_ADAPTER_REQUIRED","Explicit trusted deployment adapter required.")}const manifest=options.command==="status"?void 0:parseManifest(await readCliJson(options.manifestPath));if(options.adapterPath)return runLiveCommand(options,manifest,signal);if(options.command==="status"){return{value:{mode:"OFFLINE_SNAPSHOT",resources:snapshots(await readCliJson(options.snapshotPath))},exitCode:0}}if(options.command==="validate")return{value:{valid:true,manifest},exitCode:0};const plan=planProvisioning(manifest,snapshots(await readCliJson(options.snapshotPath)));const verified=plan.operations.every(o=>o.action==="NOOP");return{value:{mode:"OFFLINE_SNAPSHOT",verified,plan},exitCode:options.command==="verify"&&!verified||plan.operations.some(o=>o.action==="BLOCKED")?3:0}}import{boundedOperation as boundedOperation5}from"@eventsdk/sdk/core";import{AckPolicy,DeliverPolicy,RetentionPolicy,ReplayPolicy,StorageType}from"@nats-io/jetstream";function metadata(r){return{"eventsdk.owner":r.owner,"eventsdk.deployment-domain":r.deploymentDomainRef}}function nanos(seconds){const value=seconds*1e9;if(!Number.isSafeInteger(value))throw new ProvisionerError("INVALID_MANIFEST","Duration exceeds exact NATS nanoseconds.");return value}function streamCreate(r){return{name:r.name,subjects:[...r.subjects],max_age:nanos(r.retentionSeconds),max_bytes:r.maxBytes,num_replicas:r.replicas,retention:RetentionPolicy.Limits,storage:StorageType.File,metadata:metadata(r)}}function consumerCreate(r){return{durable_name:r.name,name:r.name,filter_subjects:[...r.subjects],ack_policy:AckPolicy.Explicit,deliver_policy:DeliverPolicy.All,replay_policy:ReplayPolicy.Instant,ack_wait:nanos(r.ackWaitSeconds),max_ack_pending:r.maxAckPending,metadata:metadata(r)}}function assertSafeUpdate(current,next){if(current.kind!==next.kind||current.name!==next.name||current.owner!==next.owner||current.deploymentDomainRef!==next.deploymentDomainRef||digest(current.subjects)!==digest(next.subjects))throw new ProvisionerError("UNSAFE_UPDATE","Resource identity and routing are immutable.");if(current.kind==="stream"&&next.kind==="stream"){if(next.retentionSeconds<current.retentionSeconds||next.maxBytes<current.maxBytes||next.replicas<current.replicas)throw new ProvisionerError("UNSAFE_UPDATE","Durability reduction is forbidden.")}if(current.kind==="consumer"&&next.kind==="consumer"){if(next.stream!==current.stream||next.ackWaitSeconds!==current.ackWaitSeconds||next.maxAckPending<current.maxAckPending)throw new ProvisionerError("UNSAFE_UPDATE","Consumer position and policy are immutable.")}}function streamUpdate(current,next){return{...structuredClone(current),max_age:nanos(next.retentionSeconds),max_bytes:next.maxBytes,num_replicas:next.replicas}}async function createResource(manager,resource2,issue){if(resource2.kind==="stream"){const config=streamCreate(resource2);issue();await manager.streams.add(config)}else{const config=consumerCreate(resource2);issue();await manager.consumers.add(resource2.stream,config)}}async function updateResource(manager,resource2,observed,issue){if(resource2.kind==="stream"){const info=await manager.streams.info(resource2.name);assertNativeDigest(info.config,observed);const config=streamUpdate(info.config,resource2);issue();await manager.streams.update(resource2.name,config)}else{const info=await manager.consumers.info(resource2.stream,resource2.name);assertNativeDigest(info.config,observed);const config={...structuredClone(info.config),max_ack_pending:resource2.maxAckPending};issue();await manager.consumers.update(resource2.stream,resource2.name,config)}}function assertNativeDigest(config,observed){if(!observed.nativeConfigDigest||digest(config)!==observed.nativeConfigDigest)throw new ProvisionerError("PLAN_DRIFT","Native configuration changed before write.")}import{boundedOperation as boundedOperation4}from"@eventsdk/sdk/core";var NatsWriteSession=class{constructor(timeoutMs){this.timeoutMs=timeoutMs}timeoutMs;#active;#generation=0;#busy=false;#poisoned=false;acquire(){if(this.#poisoned)throw new ProvisionerError("WRITE_SESSION_UNCERTAIN","Reconcile first.");if(this.#active)throw new ProvisionerError("MANAGEMENT_BUSY","Session already leased.");const lease=Object.freeze({generation:String(++this.#generation),release:async()=>{this.assert(lease);this.#active=void 0;if(this.#busy||this.#poisoned){this.#poisoned=true;throw new ProvisionerError("WRITE_SESSION_UNCERTAIN","Outstanding write may exist.")}}});this.#active=lease;return lease}assert(lease){if(this.#poisoned)throw new ProvisionerError("WRITE_SESSION_UNCERTAIN","Reconcile first.");if(this.#active!==lease)throw new ProvisionerError("STALE_MANAGEMENT_LEASE","Lease does not own this session.")}assertReadable(){if(this.#busy)throw new ProvisionerError("MANAGEMENT_BUSY","Mutation in progress.")}async run(lease,signal,operation2){this.assert(lease);if(this.#busy)throw new ProvisionerError("MANAGEMENT_BUSY","Mutation in progress.");this.#busy=true;let issued=false;try{await boundedOperation4({signal,timeoutMs:this.timeoutMs},async bounded=>{await operation2(bounded,()=>{bounded.throwIfAborted();this.assert(lease);issued=true});bounded.throwIfAborted();this.assert(lease)})}catch(error){if(issued)this.#poisoned=true;throw error}finally{this.#busy=false}}};var NatsExclusiveWriter=class{constructor(manager,options){this.manager=manager;const{exclusiveAccess,...data}=options;if(exclusiveAccess?.mode!=="exclusive-session"||!/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$/.test(exclusiveAccess.policyRef)||typeof exclusiveAccess.assertHeld!=="function")throw new ProvisionerError("EXCLUSIVE_ACCESS_REQUIRED","Explicit exclusive policy required.");this.#options=structuredClone(data);this.#exclusive=Object.freeze({mode:exclusiveAccess.mode,policyRef:exclusiveAccess.policyRef,assertHeld:exclusiveAccess.assertHeld.bind(exclusiveAccess)});this.#inspector=new NatsReadOnlyInspector(manager,data);this.#session=new NatsWriteSession(data.timeoutMs)}manager;#inspector;#session;#options;#exclusive;#observed;async read(signal){this.#session.assertReadable();const values=await this.#inspector.read(signal);this.#observed=structuredClone(values);return values}status(signal){return this.#inspector.status(signal)}async acquireLease(domain,signal){signal.throwIfAborted();if(domain!==this.#options.deploymentDomainRef)throw new ProvisionerError("DOMAIN_MISMATCH","Wrong management domain.");await boundedOperation5({signal,timeoutMs:this.#options.timeoutMs},bounded=>this.#exclusive.assertHeld(bounded));signal.throwIfAborted();return this.#session.acquire()}async create(config,lease,signal){await this.mutate(config,void 0,lease,signal)}async update(config,expectedDigest,lease,signal){await this.mutate(config,expectedDigest,lease,signal)}async mutate(input,expectedDigest,lease,signal){const config=parseResource(structuredClone(input));this.authorize(config);const observed=structuredClone(this.#observed);await this.#session.run(lease,signal,async(bounded,issue)=>{await this.#exclusive.assertHeld(bounded);const values=await this.#inspector.read(bounded);const current=this.validateObservation(values,observed,config,expectedDigest);await this.#exclusive.assertHeld(bounded);if(current)await updateResource(this.manager,config,current,issue);else await createResource(this.manager,config,issue);const after=await this.#inspector.read(bounded);const actual=after.find(s=>resourceKey(s.config)===resourceKey(config));if(!actual||digest(actual.config)!==digest(config))throw new ProvisionerError("WRITE_UNCONFIRMED","Written configuration did not verify.");this.#observed=structuredClone(after)})}validateObservation(values,observed,config,expectedDigest){if(!observed)throw new ProvisionerError("OBSERVATION_REQUIRED","Read authoritative configuration first.");if(configurationDigest(values)!==configurationDigest(observed))throw new ProvisionerError("PLAN_DRIFT","Configuration changed before mutation.");const current=values.find(s=>resourceKey(s.config)===resourceKey(config));if(expectedDigest===void 0){if(current)throw new ProvisionerError("RESOURCE_EXISTS","CREATE cannot replace resources.");assertParent(values,config);return void 0}if(!current||digest(current.config)!==expectedDigest)throw new ProvisionerError("PLAN_DRIFT","Expected resource configuration changed.");assertSafeUpdate(current.config,config);return current}authorize(config){const a=this.#options.authorization;const selected=this.#options.resources.some(r=>r.kind+":"+(r.stream??"")+":"+r.name===resourceKey(config));if(a.role!=="deployment"||!a.subject||!selected||config.deploymentDomainRef!==this.#options.deploymentDomainRef||a.deploymentDomainRef!==config.deploymentDomainRef||!a.allowedResourceKeys.includes(resourceKey(config))||config.subjects.some(s=>!a.allowedSubjects.includes(s)))throw new ProvisionerError("UNAUTHORIZED_DEPLOYMENT","Resource outside deployment grant.")}};function assertParent(values,config){if(config.kind!=="consumer")return;const parent=values.find(s=>s.config.kind==="stream"&&s.config.name===config.stream);if(!parent||config.subjects.some(s=>!parent.config.subjects.includes(s)))throw new ProvisionerError("PARENT_DEPENDENCY_UNPROVEN","Consumer parent is not verified.")}export{NatsExclusiveWriter,NatsReadOnlyInspector,ProvisionerError,applyProvisioning,configurationDigest,desiredResources,digest,parseManifest,parseResource,planProvisioning,provisionerConformance,provisioningStatus,resourceKey,runProvisionerCli,verifyProvisioning};
|