@ionite/ecv 0.0.23-beta.20260904.2 → 0.0.23

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.
@@ -1,7 +1,7 @@
1
- import{spawn as K8}from"node:child_process";import{trimTrailingSlash as Y8}from"@ionite/core";function Z8(X){return new Promise(($)=>setTimeout($,X))}var X8=5000,z8=2000;function L9(X,$){if(X.exitCode!==null||X.signalCode!==null)return Promise.resolve(!0);return new Promise((Q)=>{let K=!1,Y=()=>{if(K)return;K=!0,clearTimeout(Z),Q(!0)},Z=setTimeout(()=>{if(K)return;K=!0,X.removeListener("exit",Y),Q(!1)},$);X.once("exit",Y)})}var G8="/api/ionite";function L1(X){if(X.readyPath)return X.readyPath;return`${Y8(X.basePath??G8)}/readyz`}async function E1(X,$={}){let{livenessPath:Q,maxRetries:K=30,retryDelayMs:Y=1000,requestTimeoutMs:Z=3000}=$,G=L1($),W;for(let J=0;J<K;J++){try{if(Q){let D=await fetch(`${X}${Q}`,{signal:AbortSignal.timeout(Z)});if(!D.ok)throw Error(`liveness returned ${D.status}`)}let j=await fetch(`${X}${G}`,{signal:AbortSignal.timeout(Z)});if(j.ok)return;W=Error(`readiness returned ${j.status}`)}catch(j){W=j}if(J<K-1)await Z8(Y)}let H=W instanceof Error?W.message:String(W);throw Error(`App not ready at ${X}${G} after ${K} attempts: ${H}`)}async function W8(X){let{command:$,args:Q=[],cwd:K,env:Y,baseUrl:Z,...G}=X,W=process.platform!=="win32",H=K8($,Q,{cwd:K,env:{...process.env,...Y},stdio:"inherit",detached:W});H.on("error",(P)=>{console.error(`Failed to start app under test (${$}):`,P)});let J=(P)=>{try{if(W&&H.pid!==void 0)process.kill(-H.pid,P);else H.kill(P)}catch{}},j=()=>J("SIGKILL");process.once("exit",j);let D=async()=>{if(H.exitCode===null&&H.signalCode===null){if(J("SIGTERM"),!await L9(H,X8))J("SIGKILL"),await L9(H,z8)}J("SIGKILL"),process.removeListener("exit",j)};try{await E1(Z,G)}catch(P){throw await D(),P}return{baseUrl:Z,...H.pid===void 0?{}:{pid:H.pid},stop:D}}import{accessRefId as V9,accessRootId as x0,assertRequiredSubjectType as H8,CatalogAdjacencyCache as N9,deepEqualPlain as G2,defineCatalog as W2,escapeScimFilterValue as h1,expandRoot as d0,hasAllScope as P5,hasAnyScope as T9,hasPermission as _0,hasScope as q5,InMemoryAccessAssignmentStore as v,InMemoryAccessCatalogStore as Z0,InMemoryAccessIndex as U,InMemoryResourceStore as F5,InMemoryUserStore as J8,ION_AUTHZ_GROUP_SCHEMA as W0,ION_AUTHZ_OAA_SYNC as j5,ION_IAM_GROUPS_WRITE as V0,ION_IAM_READ as H1,ION_IAM_USERS_WRITE as x9,ION_TENANT_MANAGE as k1,ionite as _,stableConfigHash as N1,unionCatalogRead as O9}from"@ionite/server";import{createValidators as R}from"@ionite/valibot";function T0(X){return typeof X.baseUrl==="function"?X.baseUrl():X.baseUrl}function V1(X,$){let Q=typeof X==="function"?X():X;if(!Q)throw Error(`${$} is required`);return Q}function r(X){let $=X.timeout??5000;return async function(K,Y={}){let Z=T0(X),G=K.startsWith("http://")||K.startsWith("https://")?K:new URL(K,Z).href,W=new AbortController,H=setTimeout(()=>W.abort(),$),J=typeof X.headers==="function"?await X.headers():X.headers??{};try{return await fetch(G,{...Y,signal:W.signal,headers:{...J,...Y.headers},redirect:"manual"})}finally{clearTimeout(H)}}}async function N(X,$){let Q=performance.now();try{let K=await $();return{name:X,passed:!0,duration:performance.now()-Q,details:K?.details}}catch(K){return{name:X,passed:!1,error:K instanceof Error?K.message:String(K),duration:performance.now()-Q}}}function W1(X,$){return{name:X,passed:!0,duration:0,details:{skipped:!0,reason:$}}}function z(X,$){if(!X)throw Error($)}function w(X,$,Q){if(X!==$)throw Error(Q??`Expected ${JSON.stringify($)}, got ${JSON.stringify(X)}`)}function r0(X,$,Q){let K=$["~standard"].validate(X);if(K instanceof Promise)throw Error(Q??"Async validators are not supported in assertValid. Use the validator directly with await.");if("issues"in K){let Z=K.issues.map((G)=>{let W=G.path?G.path.map(String).join("."):"";return W?`${W}: ${G.message}`:G.message});throw Error(Q??`Validation failed: ${Z.join("; ")}`)}}function y0(X){let $=new Map,Q=X.getSetCookie?.()??[];for(let K of Q){let[Y]=K.split(";"),[Z,G]=Y.split("=");if(Z&&G!==void 0)$.set(Z.trim(),G.trim())}return $}function f0(X){return Array.from(X.entries()).map(([$,Q])=>`${$}=${Q}`).join("; ")}function h(X){return{async load(){return X},async subscribe(){return},secret:{}}}async function E(){let X=new Z0;await X.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await X.permissions.set({id:"write",name:"Write",description:"Write",source:"app"}).ready(),await X.permissions.set({id:"manage",name:"Manage",description:"Manage (cascades to sub-resources)",applyToSubResources:!0,source:"app"}).ready(),await X.roles.set({id:"editor",name:"Editor",description:"Editor",permissions:["read","write"],source:"app"}).ready(),await X.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await X.roles.set({id:"ops",name:"Ops",description:"Ops",permissions:["manage"],source:"app"}).ready(),await X.groups.set({id:"staff",providerId:"iam-staff",name:"Staff",description:"Staff",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],source:"app"}).ready(),await X.groups.set({id:"internal_staff",providerId:"iam-internal-staff",name:"Internal Staff",description:"Internal composition group",roles:["editor"],source:"app"}).ready();let $=new F5;return $.set({id:"project-a",type:"project",name:"Project A",description:"A",resources:["module-1"]}),$.set({id:"module-1",type:"module",name:"Module 1",description:"M1",parent:"project-a"}),$.set({id:"region:north",type:"region",name:"North Region",description:"North"}),$.set({id:"region:south",type:"region",name:"South Region",description:"South"}),await X.groups.set({id:"region_north_librarian",name:"North Region Librarian",description:"Requestable entitlement scoped to the North region",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],resources:["region:north"],source:"app"}).ready(),{catalog:X,resources:$}}function J1(){return W2({permissions:[{id:"code_read",name:"Code Read",description:"Read (code-declared)"},{id:"code_write",name:"Code Write",description:"Write (code-declared)"}],roles:[{id:"code_editor",name:"Code Editor",description:"Editor (code-declared)",permissions:["code_read","code_write"]}],groups:[{id:"code_staff",name:"Code Staff",description:"Staff (code-declared)",requestable:!0,assignableSubjectTypes:["user"],roles:["code_editor"]}]})}async function A(X,$,Q,K){let Y={...K,id:x0(K),status:"active"};await X.grant(Y);let Z=await d0(Y,Q);return await $.materialize(Y.id,Z),Y.id}class H2 extends U{failNext=!0;materialize(X,$){if(this.failNext)return this.failNext=!1,Promise.reject(Error("simulated transient index failure"));return super.materialize(X,$)}}class _9 extends U{replacementStartedResolve;replacementReleaseResolve;replacementStarted=new Promise((X)=>{this.replacementStartedResolve=X});replacementRelease=new Promise((X)=>{this.replacementReleaseResolve=X});async replaceSubject(X,$,Q){this.replacementStartedResolve(),await this.replacementRelease,await super.replaceSubject(X,$,Q)}releaseReplacement(){this.replacementReleaseResolve()}}class R9 extends J8{upsert(X){return Promise.reject(Error("simulated user metadata write failure"))}}function J2(){return{tests:[{name:"grant a group root materializes the full closure (group -> role -> permissions)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"alice",subjectType:"user",targetType:"group",target:"staff"}),z((await K.hasGroup("alice","staff",void 0,"user")).allow,"alice should be in group staff"),z((await K.hasRole("alice","editor",void 0,"user")).allow,"staff confers role editor"),z((await K.hasPermission("alice","read",void 0,"user")).allow,"editor confers permission read"),z((await K.hasPermission("alice","write",void 0,"user")).allow,"editor confers permission write"),z(!(await K.hasPermission("alice","manage",void 0,"user")).allow,"alice should not have manage")}},{name:"diamond survival: revoking one role root keeps permissions reachable via another root",fn:async()=>{let $=await E(),Q=new v,K=new U,Y=await A(Q,K,$,{subject:"bob",subjectType:"user",targetType:"role",target:"editor"}),Z=await A(Q,K,$,{subject:"bob",subjectType:"user",targetType:"role",target:"viewer"});z((await K.hasPermission("bob","read",void 0,"user")).allow,"read present via both roles"),await Q.revoke({subject:"bob",subjectType:"user",targetType:"role",target:"editor"}),await K.removeByRoot(Y),z((await K.hasPermission("bob","read",void 0,"user")).allow,"read survives via viewer root"),z(!(await K.hasPermission("bob","write",void 0,"user")).allow,"write removed with editor root"),await Q.revoke({subject:"bob",subjectType:"user",targetType:"role",target:"viewer"}),await K.removeByRoot(Z),z(!(await K.hasPermission("bob","read",void 0,"user")).allow,"read gone once last root removed")}},{name:"applyToSubResources expands a resource-scoped permission across the sub-resource tree",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"carol",subjectType:"user",targetType:"permission",target:"manage",resources:["project-a"]}),z((await K.hasPermission("carol","manage","project-a","user")).allow,"manage on scoped resource"),z((await K.hasPermission("carol","manage","module-1","user")).allow,"manage cascades to sub-resource"),z(!(await K.hasPermission("carol","manage","other","user")).allow,"manage not on unrelated resource"),z((await K.hasPermission("carol","manage",void 0,"user")).allow,"manage present for some resource")}},{name:"clear(subject) removes only that subject derived edges",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"dave",subjectType:"user",targetType:"role",target:"viewer"}),await A(Q,K,$,{subject:"erin",subjectType:"user",targetType:"role",target:"viewer"}),await K.clear("dave","user"),z(!(await K.hasPermission("dave","read",void 0,"user")).allow,"dave edges cleared"),z((await K.hasPermission("erin","read",void 0,"user")).allow,"erin edges retained")}},{name:"AccessAssignmentStore.list returns granted active roots",fn:async()=>{let $=new v;await $.grant({subject:"frank",subjectType:"user",targetType:"role",target:"viewer",status:"active"}),await $.grant({subject:"frank",subjectType:"user",targetType:"role",target:"editor",status:"active"});let Q=await $.list();z(Q.items.length===2,"both granted roots should be listed"),z(Q.items.every((K)=>K.targetType==="role"),"listed roots are roles")}},{name:"listByTarget returns active members of an entitlement (SCIM member projection source)",fn:async()=>{let $=new v;await $.grant({subject:"alice",subjectType:"user",targetType:"group",target:"staff",status:"active"}),await $.grant({subject:"bob",subjectType:"user",targetType:"group",target:"staff",status:"active"}),await $.grant({subject:"carol",subjectType:"user",targetType:"role",target:"viewer",status:"active"});let Q=await $.listByTarget("group","staff"),K=Q.items.map((Z)=>Z.subject).sort();z(Q.items.length===2,"both staff members are projected"),z(K[0]==="alice"&&K[1]==="bob","staff members are alice and bob"),z(Q.items.every((Z)=>Z.target==="staff"),"every projected root targets staff");let Y=await $.listByTarget("role","viewer");z(Y.items.length===1&&Y.items[0]?.subject==="carol","viewer has only carol")}},{name:"group delete semantics: revoking each listByTarget root removes membership from the closure",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"grace",subjectType:"user",targetType:"group",target:"staff"}),await A(Q,K,$,{subject:"heidi",subjectType:"user",targetType:"group",target:"staff"}),z((await K.hasPermission("grace","read",void 0,"user")).allow,"grace inherits read via staff"),z((await K.hasPermission("heidi","read",void 0,"user")).allow,"heidi inherits read via staff");let Y=await Q.listByTarget("group","staff");for(let Z of Y.items)await Q.revoke(Z),await K.removeByRoot(Z.id??x0(Z));z(!(await K.hasGroup("grace","staff",void 0,"user")).allow,"grace no longer in staff"),z(!(await K.hasPermission("heidi","read",void 0,"user")).allow,"heidi loses inherited read"),z((await Q.listByTarget("group","staff")).items.length===0,"no staff membership roots remain")}},{name:"role/permission entitlement roots round-trip through listByTarget by type",fn:async()=>{let $=new v;await $.grant({subject:"ivan",subjectType:"user",targetType:"role",target:"admin",status:"active"}),await $.grant({subject:"judy",subjectType:"user",targetType:"permission",target:"admin",status:"active"});let Q=await $.listByTarget("role","admin"),K=await $.listByTarget("permission","admin");z(Q.items.length===1&&Q.items[0]?.subject==="ivan","role admin has ivan"),z(K.items.length===1&&K.items[0]?.subject==="judy","permission admin has judy")}},{name:"resource-bound group entitlement scopes the conferred role/permissions to its resources (member-add)",fn:async()=>{let $=await E(),Q=new v,K=new U,Z=((await $.catalog.groups.get("region_north_librarian"))?.resources??[]).map((G)=>typeof G==="string"?G:G.id);z(Z.length===1&&Z[0]==="region:north","entitlement carries the region binding"),await A(Q,K,$,{subject:"nina",subjectType:"user",targetType:"group",target:"region_north_librarian",resources:Z}),z((await K.hasRole("nina","editor","region:north","user")).allow,"editor scoped to region:north"),z((await K.hasPermission("nina","read","region:north","user")).allow,"read scoped to region:north"),z((await K.hasPermission("nina","write","region:north","user")).allow,"write scoped to region:north"),z(!(await K.hasPermission("nina","read","region:south","user")).allow,"access does not leak to a different region"),z(!(await K.hasRole("nina","editor","region:south","user")).allow,"role does not leak to region:south")}},{name:"AccessCatalogStore list honors the requestable filter (SCIM assignable set)",fn:async()=>{let{catalog:$}=await E(),Q=await $.groups.list({filter:{requestable:!0}});z(Q.items.every((Y)=>Y.requestable===!0),"requestable filter returns only requestable groups"),z(Q.items.some((Y)=>Y.id==="region_north_librarian"),"the requestable region entitlement is included"),z(!Q.items.some((Y)=>Y.id==="internal_staff"),"the internal staff group is excluded");let K=await $.groups.list({filter:{requestable:!1}});z(K.items.some((Y)=>Y.id==="internal_staff"),"internal_staff (requestable unset) is in the non-requestable set"),z(!K.items.some((Y)=>Y.id==="region_north_librarian"),"the requestable entitlement is excluded from the non-requestable set")}},{name:"AccessCatalogStore does not emit a reactive change for a value-identical set",fn:async()=>{let $=new Z0,Q=0,K=$.afterChange?.(()=>{Q+=1}),Y={id:"stable-group",name:"Stable Group",description:"No-op write proof",requestable:!0,metadata:{first:1,second:2}};await $.groups.set(Y).ready(),await $.groups.set({requestable:!0,description:"No-op write proof",name:"Stable Group",id:"stable-group",metadata:{second:2,first:1}}).ready(),K?.(),w(Q,1,"a key-reordered but value-identical set emits no second change")}},{name:"CodeCatalog union reads propagate backing-store failures instead of serving partial inventory",fn:async()=>{let $=async(W,H)=>{let J=!1;try{await W()}catch{J=!0}z(J,H)},Q=J1(),K=new Z0;K.groups.get=async()=>{throw Error("get unavailable")};let Y=O9(Q,K);z(Y!==void 0,"the code/store union is available"),await $(()=>Y.groups.get("code_staff"),"get failure must propagate");let Z=new Z0;Z.groups.list=async()=>{throw Error("list unavailable")};let G=O9(Q,Z);z(G!==void 0,"the code/store union is available"),await $(()=>G.groups.list(),"list failure must propagate")}},{name:"ion.authz.oaa becomes ready once OAA config (url/providerId/datasourceId) is applied",fn:async()=>{let $=_(h({id:"oaa-ready-test",authz:{oaa:{url:"https://veza.example",providerId:"prov",datasourceId:"ds"}}}),{validators:R()});z(!$.authz.oaa.isReady(),"OAA should not be ready before RemoteConfig is applied"),await $.ready(1000),z($.authz.oaa.isReady(),"OAA should be ready once url/providerId/datasourceId are present"),await $.authz.oaa.ready(1000),z($.readiness().modules.authz.ready===!0,"authz is ready once OAA is configured and applied"),$.close?.()}},{name:"ion.authz.oaa stays not ready and sync throws not_configured without OAA config",fn:async()=>{let $=_(h({id:"no-oaa-test"}),{validators:R()});await $.ready(1000),z(!$.authz.oaa.isReady(),"OAA should not be ready when no OAA config is present");let Q=$.readiness();z(Q.modules.authz.active===!0,"authz module is always active"),z(Q.modules.authz.ready===!0,"authz is ready when OAA is not configured"),z($.requireReady(["authz"]).ok===!0,"requireReady([authz]) is ok without OAA configured");let K;try{await $.authz.oaa.sync({})}catch(Y){K=Y.code}z(K==="not_configured","sync should throw OaaError with code not_configured when unconfigured"),$.close?.()}},{name:"ion.authz.has* delegate to the wired AccessIndex (allow / deny)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"alice",subjectType:"user",targetType:"role",target:"editor"});let Y=_(h({id:"authz-has-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasRole("alice","editor",void 0,"user")).allow,"ion.authz.hasRole resolves a granted role"),z((await Y.authz.hasPermission("alice","read",void 0,"user")).allow,"ion.authz.hasPermission resolves a permission conferred by the role"),z(!(await Y.authz.hasPermission("alice","manage",void 0,"user")).allow,"ion.authz.hasPermission denies a permission the subject was never granted"),Y.close?.()}},{name:"ion.authz.has* deny by default (not_configured) when no AccessIndex is wired",fn:async()=>{let $=_(h({id:"authz-noindex-test"}),{validators:R()});await $.ready(1000);let Q=await $.authz.hasPermission("alice","read",void 0,"user");z(!Q.allow,"denies when no AccessIndex is configured"),z(Q.metadata?.reason==="access_index_not_configured","decision metadata explains the missing AccessIndex"),$.close?.()}},{name:"standalone hasPermission(request, ion) returns unauthenticated -> 401 without a session",fn:async()=>{let $=_(h({id:"authz-helper-test"}),{validators:R()});await $.ready(1000);let Q=await _0(new Request("https://example.test/"),$,"read");z(!Q.allow,"no SSO session -> not allowed"),z(Q.reason==="unauthenticated","reason is unauthenticated when no user is resolved"),z(Q.response().status===401,"response() is 401 for the unauthenticated case"),$.close?.()}},{name:"subjectType isolates principals that share an id (user:123 vs customer:123)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"123",subjectType:"user",targetType:"role",target:"editor"}),await A(Q,K,$,{subject:"123",subjectType:"customer",targetType:"role",target:"viewer"}),z((await K.hasRole("123","editor",void 0,"user")).allow,"user:123 has editor"),z((await K.hasPermission("123","write",void 0,"user")).allow,"user:123 has write via editor"),z(!(await K.hasRole("123","editor",void 0,"customer")).allow,"customer:123 does NOT have editor"),z((await K.hasRole("123","viewer",void 0,"customer")).allow,"customer:123 has viewer"),z(!(await K.hasRole("123","viewer",void 0,"user")).allow,"user:123 does NOT have viewer"),z(!(await K.hasPermission("123","write",void 0,"customer")).allow,"customer:123 has no write"),await K.clear("123","user"),z(!(await K.hasRole("123","editor",void 0,"user")).allow,"user:123 edges cleared"),z((await K.hasRole("123","viewer",void 0,"customer")).allow,"customer:123 edges retained")}},{name:"ion.authz.has* honor subjectType (a customer grant is invisible to a user check)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"cust-1",subjectType:"customer",targetType:"role",target:"viewer"});let Y=_(h({id:"authz-subjecttype-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasRole("cust-1","viewer",void 0,"customer")).allow,"customer grant resolves under the customer subjectType"),z((await Y.authz.hasPermission("cust-1","read",void 0,"customer")).allow,"customer inherits read via viewer"),z(!(await Y.authz.hasRole("cust-1","viewer",void 0,"user")).allow,"same id under user subjectType is denied"),z(!(await Y.authz.hasRole("cust-1","viewer",void 0,"user")).allow,"same id under explicit user subjectType is denied"),Y.close?.()}},{name:"standalone has* default 'any' resolves no principals without a session -> unauthenticated/401",fn:async()=>{let $=await E(),Q=new v,K=new U,Y=_(h({id:"authz-any-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000);let Z=await _0(new Request("https://example.test/"),Y,"read");z(!Z.allow,"default 'any' with no session is not allowed"),z(Z.reason==="unauthenticated","no principal resolved -> unauthenticated"),z(Z.subject===void 0,"no subject is reported when nothing resolved"),z(Z.subjectType===void 0,"no subjectType is reported when nothing resolved"),z(Z.response().status===401,"unauthenticated -> 401"),Y.close?.()}},{name:"subjectType isolates a workload from a user that shares an id (workload:abc vs user:abc)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"abc",subjectType:"workload",targetType:"role",target:"ops"}),await A(Q,K,$,{subject:"abc",subjectType:"user",targetType:"role",target:"viewer"}),z((await K.hasRole("abc","ops",void 0,"workload")).allow,"workload:abc has ops"),z((await K.hasPermission("abc","manage",void 0,"workload")).allow,"workload:abc has manage via ops"),z(!(await K.hasRole("abc","ops",void 0,"user")).allow,"user:abc does NOT have ops"),z(!(await K.hasPermission("abc","manage",void 0,"user")).allow,"user:abc does NOT inherit manage"),z((await K.hasRole("abc","viewer",void 0,"user")).allow,"user:abc has viewer"),z(!(await K.hasRole("abc","viewer",void 0,"workload")).allow,"workload:abc does NOT have viewer"),await K.clear("abc","workload"),z(!(await K.hasRole("abc","ops",void 0,"workload")).allow,"workload:abc edges cleared"),z((await K.hasRole("abc","viewer",void 0,"user")).allow,"user:abc edges retained")}},{name:"ion.authz.has* honor the workload subjectType (a workload grant is invisible to a user check)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"svc-1",subjectType:"workload",targetType:"role",target:"ops"});let Y=_(h({id:"authz-workload-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasRole("svc-1","ops",void 0,"workload")).allow,"workload grant resolves under the workload subjectType"),z((await Y.authz.hasPermission("svc-1","manage",void 0,"workload")).allow,"workload inherits manage via ops"),z(!(await Y.authz.hasRole("svc-1","ops",void 0,"user")).allow,"same id under user subjectType is denied"),Y.close?.()}},{name:"accessRootId and store writes fail closed when subjectType is omitted",fn:async()=>{let $=!1;try{x0({subject:"x",targetType:"role",target:"viewer"})}catch(K){$=K instanceof Error&&K.message.includes("subjectType is required")}z($,"accessRootId rejects a missing subjectType instead of defaulting to user"),$=!1;try{H8(void 0)}catch(K){$=K instanceof Error&&K.message.includes("subjectType is required")}z($,"assertRequiredSubjectType rejects undefined");let Q=new v;$=!1;try{await Q.grant({subject:"x",targetType:"role",target:"viewer"})}catch(K){$=K instanceof Error&&K.message.includes("subjectType is required")}z($,"assignment store grant rejects a missing subjectType")}},{name:"standalone hasScope* return unauthenticated -> 401 when no workload token is present",fn:async()=>{let $=_(h({id:"authz-scope-noauth-test"}),{validators:R()});await $.ready(1000);let Q=new Request("https://example.test/"),K=await q5(Q,$,"oaa:sync","workload");z(!K.allow&&K.reason==="unauthenticated","hasScope with no token -> unauthenticated"),z(K.response().status===401,"hasScope unauthenticated -> 401");let Y=await T9(Q,$,["a","b"],"workload");z(!Y.allow&&Y.reason==="unauthenticated","hasAnyScope with no token -> unauthenticated");let Z=await P5(Q,$,["a","b"],"workload");z(!Z.allow&&Z.reason==="unauthenticated","hasAllScope with no token -> unauthenticated"),$.close?.()}},{name:"RemoteConfig.authz.scopesAsEntitlements is read into ion.config (gates has* scope auto-honor)",fn:async()=>{let $=_(h({id:"scopes-flag-on",authz:{scopesAsEntitlements:!0}}),{validators:R()});await $.ready(1000),z($.config?.authz?.scopesAsEntitlements===!0,"flag true is applied to ion.config"),$.close?.();let Q=_(h({id:"scopes-flag-off"}),{validators:R()});await Q.ready(1000),z(!Q.config?.authz?.scopesAsEntitlements,"flag defaults to falsy when unset (secure default)"),Q.close?.()}},{name:"ion.authz.reconcile() re-derives the closure after a role->permission catalog edit (§1.2a)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"drift",subjectType:"user",targetType:"role",target:"viewer"});let Y=_(h({id:"reconcile-roleedit"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasPermission("drift","read",void 0,"user")).allow,"viewer confers read"),z(!(await Y.authz.hasPermission("drift","write",void 0,"user")).allow,"viewer does not confer write yet"),await $.catalog.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read","write"]}).ready();let Z=await Y.authz.reconcile();z(Z.roots===1,"one live root re-expanded (only subject in the system)"),z((await Y.authz.hasPermission("drift","write",void 0,"user")).allow,"reconcile materializes the new write edge"),z((await Y.authz.hasPermission("drift","read",void 0,"user")).allow,"read still present after reconcile"),Y.close?.()}},{name:"ion.authz.reconcile() extends an applyToSubResources closure to a newly added child resource (§1.2a)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"tree",subjectType:"user",targetType:"permission",target:"manage",resources:["project-a"]});let Y=_(h({id:"reconcile-tree"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasPermission("tree","manage","module-1","user")).allow,"manage cascades to the existing child module-1"),z(!(await Y.authz.hasPermission("tree","manage","module-2","user")).allow,"module-2 does not exist yet"),$.resources.set({id:"module-2",type:"module",name:"Module 2",description:"M2",parent:"project-a"}),$.resources.set({id:"project-a",type:"project",name:"Project A",description:"A",resources:["module-1","module-2"]}),await Y.authz.reconcile(),z((await Y.authz.hasPermission("tree","manage","module-2","user")).allow,"reconcile cascades manage to the newly added child"),Y.close?.()}},{name:"ion.authz.reconcile() rebuilds set-attributed edges so an overlapping permission survives one revoke",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"olap",subjectType:"user",targetType:"role",target:"editor"}),await A(Q,K,$,{subject:"olap",subjectType:"user",targetType:"role",target:"viewer"});let Y=_(h({id:"reconcile-overlap"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),await Q.revoke({subject:"olap",subjectType:"user",targetType:"role",target:"editor"});let Z=await Y.authz.reconcile();z(Z.roots===1,"only the surviving viewer root remains live"),z((await Y.authz.hasPermission("olap","read",void 0,"user")).allow,"read survives via the viewer root"),z(!(await Y.authz.hasPermission("olap","write",void 0,"user")).allow,"write is removed with the editor root"),Y.close?.()}},{name:"reconcile atomically swaps a subject closure without a transient deny window",fn:async()=>{let $=await E(),Q=new v,K=new _9;await A(Q,K,$,{subject:"atomic",subjectType:"user",targetType:"role",target:"editor"});let Y=_(h({id:"reconcile-atomic"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),await Q.revoke({subject:"atomic",subjectType:"user",targetType:"role",target:"editor"});let Z={subject:"atomic",subjectType:"user",targetType:"role",target:"viewer",status:"active"};Z.id=x0(Z),await Q.grant(Z);let G=Y.authz.reconcile();await K.replacementStarted;try{z((await K.hasPermission("atomic","write",void 0,"user")).allow,"the previous complete closure remains visible while its replacement is pending")}finally{K.releaseReplacement()}await G,z((await K.hasPermission("atomic","read",void 0,"user")).allow,"the replacement viewer closure is committed"),z(!(await K.hasPermission("atomic","write",void 0,"user")).allow,"the stale editor-only permission disappears at the atomic swap"),Y.close?.()}},{name:"partial-failure retry converges: a failed index materialize is repaired by re-grant + re-materialize (replaces §1.2b)",fn:async()=>{let $=await E(),Q=new v,K=new H2,Y={subject:"retry",subjectType:"user",targetType:"role",target:"viewer",status:"active"};Y.id=x0(Y),await Q.grant(Y);let Z=!1;try{let W=await d0(Y,$);await K.materialize(Y.id,W)}catch{Z=!0}z(Z,"the first index write fails (simulated transient error)"),z(!(await K.hasPermission("retry","read",void 0,"user")).allow,"no derived access after the failed attempt"),await Q.grant(Y);let G=await d0(Y,$);await K.materialize(Y.id,G),z((await K.hasPermission("retry","read",void 0,"user")).allow,"retry re-materializes the closure"),z((await Q.listBySubject("retry","user")).items.length===1,"exactly one root persisted (grant is idempotent; no duplicate)"),await K.removeByRoot(Y.id),z(!(await K.hasPermission("retry","read",void 0,"user")).allow,"access cleanly removed; no orphan rows linger")}},{name:"revoke idempotency: revoking an absent root is a no-op and a retried revoke still denies",fn:async()=>{let $=await E(),Q=new v,K=new U,Y=await A(Q,K,$,{subject:"idem",subjectType:"user",targetType:"role",target:"viewer"});z((await K.hasPermission("idem","read",void 0,"user")).allow,"granted read is present"),await Q.revoke({subject:"idem",subjectType:"user",targetType:"role",target:"viewer"}),await K.removeByRoot(Y),z(!(await K.hasPermission("idem","read",void 0,"user")).allow,"read denied after revoke"),await Q.revoke({subject:"idem",subjectType:"user",targetType:"role",target:"viewer"}),await K.removeByRoot(Y),z((await Q.listBySubject("idem","user")).items.length===0,"no roots remain after idempotent revoke"),z(!(await K.hasPermission("idem","read",void 0,"user")).allow,"retry of an absent revoke keeps access denied")}},{name:"user delete semantics (§1.19): revoking each listBySubject root removes the subject from the closure",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"leaver",subjectType:"user",targetType:"group",target:"staff"}),await A(Q,K,$,{subject:"leaver",subjectType:"user",targetType:"role",target:"ops"}),z((await K.hasPermission("leaver","read",void 0,"user")).allow,"leaver inherits read via staff"),z((await K.hasPermission("leaver","manage",void 0,"user")).allow,"leaver holds manage via ops");let Y=await Q.listBySubject("leaver","user");z(Y.items.length===2,"both of the leaver roots are enumerated by listBySubject");for(let Z of Y.items)await Q.revoke(Z),await K.removeByRoot(Z.id??x0(Z));z(!(await K.hasGroup("leaver","staff",void 0,"user")).allow,"group membership revoked on delete"),z(!(await K.hasPermission("leaver","read",void 0,"user")).allow,"inherited read denied on delete"),z(!(await K.hasPermission("leaver","manage",void 0,"user")).allow,"role permission denied on delete"),z((await Q.listBySubject("leaver","user")).items.length===0,"no assignment roots remain")}},{name:"ion.authz.reconcile() rebuilds live subjects from roots and sweeps orphan index rows",fn:async()=>{let $=await E(),Q=new v,K=new U,Y={subject:"live",subjectType:"user",targetType:"role",target:"editor",status:"active"};Y.id=x0(Y),await Q.grant(Y),z(!(await K.hasPermission("live","read",void 0,"user")).allow,"live subject has no index rows pre-reconcile"),await K.materialize("orphan-root",[{subject:"orphan",subjectType:"user",targetType:"role",target:"viewer",rootId:"orphan-root"},{subject:"orphan",subjectType:"user",targetType:"permission",target:"read",rootId:"orphan-root"}]),z((await K.hasPermission("orphan","read",void 0,"user")).allow,"orphan rows are present pre-reconcile");let Z=_(h({id:"reconcile-full"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Z.ready(1000);let G=await Z.authz.reconcile();z(G.subjects>=2,"the full reconcile visited both the live and orphan subjects"),z((await Z.authz.hasPermission("live","read",void 0,"user")).allow,"full reconcile materializes the live root"),z((await Z.authz.hasPermission("live","write",void 0,"user")).allow,"editor closure rebuilt for the live subject"),z(!(await Z.authz.hasPermission("orphan","read",void 0,"user")).allow,"orphan index rows are swept"),z(G.cleared>=1,"the orphan subject is reported as cleared"),Z.close?.()}},{name:"§8.4 id parity: two roots with the same subject/target but different resources do NOT collapse",fn:async()=>{let $={subject:"pat",subjectType:"user",targetType:"role",target:"editor",resources:["region:north"]},Q={subject:"pat",subjectType:"user",targetType:"role",target:"editor",resources:["region:south"]};z(x0($)!==x0(Q),"different resource scopes yield different root ids"),z(x0($)===x0({...$}),"same inputs yield a stable root id");let K=new v;await K.grant({...$,id:x0($),status:"active"}),await K.grant({...Q,id:x0(Q),status:"active"});let Y=await K.listByTarget("role","editor");z(Y.items.length===2,"both resource-scoped roots coexist (no collapse)"),z(V9(" editor ")==="editor","accessRefId trims string ids"),z(V9({id:"editor"})==="editor","accessRefId resolves an object ref by id")}},{name:"§7.4 listByTargets batches a page of member projections into one store call (no N+1)",fn:async()=>{let $=0,Q=0;class K extends v{async listByTarget(...G){return $+=1,super.listByTarget(...G)}async listByTargets(...G){return Q+=1,super.listByTargets(...G)}}let Y=new K;await Y.grant({subject:"m1",subjectType:"user",targetType:"group",target:"g1",status:"active"}),await Y.grant({subject:"m2",subjectType:"user",targetType:"group",target:"g1",status:"active"}),await Y.grant({subject:"m3",subjectType:"user",targetType:"role",target:"r1",status:"active"});let Z=await Y.listByTargets([{targetType:"group",target:"g1"},{targetType:"role",target:"r1"},{targetType:"permission",target:"p1"}]);z(Q===1,"one batched call covers the whole page"),z($===0,"the batched path does not fan out to per-item listByTarget"),z((Z.get("group:g1")??[]).length===2,"g1 members batched"),z((Z.get("role:r1")??[]).length===1,"r1 members batched"),z((Z.get("permission:p1")??[]).length===0,"an empty target returns an empty list")}},{name:"§7.4 hasAll over N targets resolves in one pass (bounded) and denies if any is missing",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"multi",subjectType:"user",targetType:"role",target:"editor"}),await A(Q,K,$,{subject:"multi",subjectType:"user",targetType:"role",target:"viewer"}),await A(Q,K,$,{subject:"multi",subjectType:"user",targetType:"role",target:"ops"});let Y=["editor","viewer","ops"].map((G)=>({targetType:"role",target:G}));z((await K.hasAll("multi",Y,"user")).allow,"hasAll is satisfied when every target is held");let Z=[...Y,{targetType:"role",target:"admin"}];z(!(await K.hasAll("multi",Z,"user")).allow,"hasAll denies when one target is missing")}},{name:"cross-tenant isolation: grants in one store keyspace are invisible to another (distinct stores)",fn:async()=>{let $=await E(),Q={store:new v,index:new U},K={store:new v,index:new U};await A(Q.store,Q.index,$,{subject:"shared-123",subjectType:"user",targetType:"role",target:"ops"}),z((await Q.index.hasRole("shared-123","ops",void 0,"user")).allow,"tenant A granted ops to shared-123"),z(!(await K.index.hasRole("shared-123","ops",void 0,"user")).allow,"tenant B (separate keyspace) does NOT see tenant A grant for the same id"),z((await K.store.listBySubject("shared-123","user")).items.length===0,"tenant B store has no roots for the shared id")}},{name:"§7.4 CatalogAdjacencyCache memoizes catalog reads and re-reads after invalidation (same edges)",fn:async()=>{let $=await E(),Q={subject:"memo",subjectType:"user",targetType:"group",target:"staff"};Q.id=x0(Q);let K=await d0(Q,$),Y=new N9($.catalog,$.resources),Z={...$,cache:Y},G=await d0(Q,Z),W=await d0(Q,Z);z(G2(K,G)&&G2(G,W),"cached expansion produces the same edges as the uncached read"),await $.catalog.groups.set({id:"staff",name:"Staff",description:"Staff",roles:["editor","ops"]}).ready();let H=await d0(Q,Z);z(G2(H,G),"a not-yet-invalidated memo still returns the old closure"),Y.invalidate();let J=await d0(Q,Z);z(J.some((j)=>j.targetType==="permission"&&j.target==="manage"),"after invalidation the cache re-reads the catalog and reflects the new ops->manage edge")}},{name:"code-declared catalog (defineCatalog) expands a grant with NO catalogStore wired (zero store reads)",fn:async()=>{let $=J1(),Q=new v,K=new U;await A(Q,K,{code:$},{subject:"carol",subjectType:"user",targetType:"group",target:"code_staff"}),z((await K.hasGroup("carol","code_staff",void 0,"user")).allow,"carol is in the code-declared group"),z((await K.hasRole("carol","code_editor",void 0,"user")).allow,"the code group confers the code role"),z((await K.hasPermission("carol","code_read",void 0,"user")).allow,"the code role confers code_read"),z((await K.hasPermission("carol","code_write",void 0,"user")).allow,"the code role confers code_write"),z($.groups.get("code_staff")?.id==="code_staff","the App-owned catalog id remains stable")}},{name:"defineCatalog rejects duplicate ids, dangling references, and group cycles at module evaluation",fn:async()=>{let $=(Q)=>{try{return Q(),!1}catch{return!0}};z($(()=>W2({roles:[{id:"dup",name:"Dup",description:"dup"}],permissions:[{id:"dup",name:"Dup",description:"dup"}]})),"a duplicate id across kinds throws (one SCIM Groups id space)"),z($(()=>W2({groups:[{id:"g",name:"G",description:"g",roles:["missing_role"]}]})),"a dangling group->role reference throws"),z($(()=>W2({groups:[{id:"a",name:"A",description:"a",groups:["b"]},{id:"b",name:"B",description:"b",groups:["a"]}]})),"a nested-group cycle throws")}},{name:"§7.2 stableConfigHash is order-independent and agrees with deepEqualPlain equality",fn:async()=>{let $={id:"t",authz:{scopesAsEntitlements:!0},workload:{jwksUri:"https://x/certs"}},Q={workload:{jwksUri:"https://x/certs"},authz:{scopesAsEntitlements:!0},id:"t"},K={...$,extra:void 0},Y={...$,id:"t2"};z(G2($,Q),"precondition: the two objects are deep-equal"),z(N1($)===N1(Q),"key order does not change the hash"),z(N1($)===N1(K),"undefined-valued keys do not change the hash"),z(N1($)!==N1(Y),"a changed value changes the hash")}},{name:'§10.3 escapeScimFilterValue backslash-escapes `"` and `\\` for RFC 7644 filters',fn:async()=>{w(h1('a"b'),"a\\\"b","a double quote is backslash-escaped so it stays inside the filter literal"),w(h1("a\\b"),"a\\\\b","a backslash is doubled so the escape is unambiguous"),w(h1("x\\\"y"),"x\\\\\\\"y","ordering escapes backslash first, then quote"),w(h1("plain-id"),"plain-id","an ordinary id is returned unchanged");let $=`externalId eq "${h1('ev"il')}"`;w($,'externalId eq "ev\\"il"',"the composed filter carries the escaped literal")}},{name:"§7.3 a shared CatalogAdjacencyCache collapses N member expansions to O(graph) catalog reads",fn:async()=>{let $=new Z0;await $.permissions.set({id:"read",name:"Read",description:"Read"}).ready(),await $.permissions.set({id:"write",name:"Write",description:"Write"}).ready(),await $.roles.set({id:"editor",name:"Editor",description:"Editor",permissions:["read","write"]}).ready(),await $.groups.set({id:"staff",name:"Staff",description:"Staff",roles:["editor"]}).ready();let Q=0,K=(j)=>({...j,get:async(D)=>{return Q+=1,j.get(D)}}),Y={groups:K($.groups),roles:K($.roles),permissions:K($.permissions)},Z=Array.from({length:100},(j,D)=>`m${D}`),G=(j)=>({subject:j,subjectType:"user",targetType:"group",target:"staff",status:"active",id:`r-${j}`});Q=0;for(let j of Z)await d0(G(j),{catalog:Y});let W=Q,H=new N9(Y);Q=0;for(let j of Z)await d0(G(j),{catalog:Y,cache:H});let J=Q;z(W>=Z.length,`uncached expansion re-reads the graph per member (saw ${W})`),z(J<W,"a shared adjacency cache must collapse repeated node reads"),z(J<=8,`cached expansion reads the shared graph O(graph) once, not O(members×graph) (saw ${J})`)}}]}}function v9(X){let $=typeof X==="function"?X():X;if(!$)throw Error("AuthzValidationConfig.ecv is required to run OAA projection tests");return $}function L5(X,$){return h({id:"ecv-oaa-projection",authz:{oaa:{url:X.baseUrl,apiKey:X.oaa.apiKey,providerId:X.oaa.providerId,datasourceId:X.oaa.datasourceId,applicationName:"ECV OAA Projection"},...$?{trustedProvisioningClients:$}:{}}})}async function l0(X,$,Q,K){let Y=_(L5(X,K),{validators:R()});try{return await Y.ready($),await Q(Y)}finally{Y.close?.()}}async function w5(X,$,Q,K,Y){let Z=_(L5(X),{validators:R(),stores:{catalogStore:Q,...Y?{resources:Y}:{}}});try{return await Z.ready($),await K(Z)}finally{Z.close?.()}}function C1(X,$){return h({id:"ecv-oaa-fullsync",workload:{jwksUri:`${X.baseUrl}/workload/certs`,audience:`${X.baseUrl}/workload`},authz:{scopesAsEntitlements:!0,oaa:{url:X.baseUrl,apiKey:X.oaa.apiKey,providerId:X.oaa.providerId,datasourceId:X.oaa.datasourceId,applicationName:"ECV OAA Full Sync",fullSync:{enabled:$}}}})}function D5(X,$){return h({id:$,workload:{jwksUri:`${X.baseUrl}/workload/certs`,audience:`${X.baseUrl}/workload`},iam:{},authz:{scopesAsEntitlements:!0,oaa:{url:X.baseUrl,apiKey:X.oaa.apiKey,providerId:X.oaa.providerId,datasourceId:X.oaa.datasourceId,applicationName:"ECV SCIM OAA Guard"}}})}var O1="https://ecv.test/api/ionite/authz/oaa/sync";function Y0(X){let $=X.applications;if(!Array.isArray($)||$.length===0)return;let Q=$[0];return Q&&typeof Q==="object"?Q:void 0}function y(X){if(!Array.isArray(X))return[];return X.filter(($)=>Boolean($)&&typeof $==="object")}function j2(X){let $=X.timeout??5000,Q=()=>v9(X.ecv);return[{name:"SCIM Group member type Workload grants a workload root and round-trips as type Workload",fn:async()=>{let K=await S(Q().baseUrl,`${V0.id} ${H1.id}`),Y=new Z0;await Y.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await Y.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await Y.groups.set({id:"vault_path_reader",name:"Path Reader",description:"Path Reader",requestable:!0,assignableSubjectTypes:["workload"],roles:["viewer"],source:"app"}).ready();let Z=new v,G=new U,W=_(h({id:"ecv-scim-workload-member",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y,accessStore:Z,accessIndex:G}});try{await W.ready(1000);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/vault_path_reader",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-vault",type:"Workload"}]}]})}));z(H.status===200,`workload member PATCH should succeed, got ${H.status}: ${await H.text()}`),z((await G.hasPermission("svc-vault","read",void 0,"workload")).allow,"Workload member satisfies a workload permission check"),z(!(await G.hasPermission("svc-vault","read",void 0,"user")).allow,"same-id user check does not see the workload grant");let J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/vault_path_reader",{headers:{Authorization:`Bearer ${K}`}}));z(J.status===200,`GET group should succeed, got ${J.status}`);let D=(await J.json()).members?.find((P)=>P.value==="svc-vault");z(D?.type==="Workload",`projected member type should be Workload, got ${D?.type}`)}finally{W.close?.()}}},{name:"SCIM typeless member resolves when assignableSubjectTypes has exactly one kind and is rejected when ambiguous",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0;await Y.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await Y.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await Y.groups.set({id:"workload_only",name:"Workload Only",description:"Workload Only",requestable:!0,assignableSubjectTypes:["workload"],roles:["viewer"],source:"app"}).ready(),await Y.groups.set({id:"open_entitlement",name:"Open Entitlement",description:"Open Entitlement",requestable:!0,roles:["viewer"],source:"app"}).ready();let Z=new v,G=new U,W=_(h({id:"ecv-scim-typeless-member",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y,accessStore:Z,accessIndex:G}});try{await W.ready(1000);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/workload_only",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-typeless"}]}]})}));z(H.status===200,`typeless member on a workload-only entitlement should resolve, got ${H.status}: ${await H.text()}`),z((await G.hasPermission("svc-typeless","read",void 0,"workload")).allow,"typeless member resolved to workload from assignableSubjectTypes");let J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/open_entitlement",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-ambiguous"}]}]})}));z(J.status===400,`typeless member on an unrestricted entitlement should 400, got ${J.status}`);let j=await J.text();z(j.includes("open_entitlement"),"ambiguity error names the entitlement"),z((await Z.listBySubject("svc-ambiguous","user")).items.length===0,"ambiguous member wrote no user root"),z((await Z.listBySubject("svc-ambiguous","workload")).items.length===0,"ambiguous member wrote no workload root")}finally{W.close?.()}}},{name:"same-id User and Workload members remain distinct roots",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0;await Y.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await Y.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await Y.groups.set({id:"shared_id_entitlement",name:"Shared Id Entitlement",description:"Shared Id Entitlement",requestable:!0,assignableSubjectTypes:["user","workload"],roles:["viewer"],source:"app"}).ready();let Z=new v,G=new U,W=_(h({id:"ecv-scim-same-id-members",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y,accessStore:Z,accessIndex:G}});try{await W.ready(1000);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/shared_id_entitlement",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"shared",type:"User"},{value:"shared",type:"Workload"}]}]})}));z(H.status===200,`same-id User+Workload PATCH should succeed, got ${H.status}: ${await H.text()}`),z((await G.hasPermission("shared","read",void 0,"user")).allow,"user:shared holds the grant"),z((await G.hasPermission("shared","read",void 0,"workload")).allow,"workload:shared holds a distinct grant");let J=await Z.listBySubject("shared","user"),j=await Z.listBySubject("shared","workload");z(J.items.length===1,"user principal has its own root"),z(j.items.length===1,"workload principal has its own root"),z(J.items[0]?.id!==j.items[0]?.id,"same-id User and Workload roots are distinct")}finally{W.close?.()}}},{name:"ion.authz.oaa is ready when pointed at the mock receiver and projects an incremental user push",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(W)=>{z(W.authz.oaa.isReady(),"OAA should be ready with mock receiver config"),await W.authz.oaa.ready($),await W.authz.oaa.pushUser({id:"ecv-user-1",userName:"ecv-user-1@example.com",displayName:"ECV User One",email:"ecv-user-1@example.com"})});let K=await Q().oaa.pushes();z(K.length>=1,"expected at least one OAA push to be received");let Y=K[K.length-1];z(Y!==void 0,"expected a recorded OAA push"),z(Y.via==="single","an incremental user push should use the single :push endpoint"),z(Y.isIncremental===!0,"a mutation-scoped push should be marked incremental"),z(Y.compressionType==="GZIP","OAA client compresses payloads with gzip by default");let Z=Y0(Y.payload);z(Z!==void 0,"OAA payload must contain an application");let G=y(Z?.local_users);z(G.some((W)=>W.unique_id==="ecv-user-1"),"the pushed user should appear in local_users")}},{name:"ion.authz.oaa projects a group definition with its role/permission assignments",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(H)=>{await H.authz.oaa.pushGroup({id:"ecv-group-1",name:"ECV Group One",description:"ECV projected group",roles:["ecv-role-1"],permissions:["ecv-perm-1"]})});let K=(await Q().oaa.pushes()).at(-1);z(K!==void 0,"expected a recorded OAA group push");let Y=Y0(K.payload),Z=y(Y?.local_groups);z(Z.some((H)=>H.unique_id==="ecv-group-1"),"the pushed group should appear in local_groups");let W=y(K.payload.identity_to_permissions).find((H)=>H.identity==="ecv-group-1");z(W!==void 0,"group role/permission assignments should project an identity entry"),z(W?.identity_type==="local_group","a catalog group is projected as a local_group principal")}},{name:"ion.authz.oaa scopes a resource-bound group role assignment (apply_to_application:false + resources)",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(D)=>{await D.authz.oaa.pushGroup({id:"ecv-region-librarian",name:"ECV Region Librarian",description:"resource-bound requestable entitlement",requestable:!0,roles:["ecv-librarian"],resources:["region:ecv-north"]})});let K=(await Q().oaa.pushes()).at(-1);z(K!==void 0,"expected a recorded OAA group push");let Z=y(K.payload.identity_to_permissions).find((D)=>D.identity==="ecv-region-librarian");z(Z!==void 0,"resource-bound group should project an identity entry");let W=y(Z?.role_assignments).find((D)=>D.role==="ecv-librarian");z(W!==void 0,"the group should confer the librarian role"),z(W?.apply_to_application===!1,"a resource-bound assignment must not be application-wide"),z(Array.isArray(W?.resources)&&(W?.resources).includes("region:ecv-north"),"the role assignment carries the bound resource");let H=Y0(K.payload),j=y(H?.local_groups).find((D)=>D.unique_id==="ecv-region-librarian")?.custom_properties??{};z(Array.isArray(j.resources)&&j.resources.includes("region:ecv-north"),"group custom_properties expose the resource scope")}},{name:"ion.authz.oaa projects a permission with canonical permission types",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(G)=>{await G.authz.oaa.pushPermission({id:"ecv-perm-1",name:"ECV Read",description:"ECV read permission",permissionTypes:["DataRead"]})});let K=(await Q().oaa.pushes()).at(-1);z(K!==void 0,"expected a recorded OAA permission push");let Z=y(K.payload.permissions).find((G)=>G.name==="ECV Read");z(Z!==void 0,"the pushed permission should appear in the permissions stream"),z(Array.isArray(Z?.permission_type)&&(Z?.permission_type).includes("DataRead"),"permission_type should carry the canonical OAA privilege")}},{name:"ion.authz.oaa projects grant then revoke of a group membership",fn:async()=>{await Q().oaa.clear();let K={subject:"ecv-user-2",subjectType:"user",targetType:"group",target:"ecv-group-1"};await l0(Q(),$,async(J)=>{await J.authz.oaa.grant(K),await J.authz.oaa.revoke(K)});let Y=await Q().oaa.pushes();z(Y.length>=2,"grant and revoke should each produce a push");let G=y(Y0(Y.at(-2)?.payload??{})?.local_users).find((J)=>J.unique_id==="ecv-user-2");z(G!==void 0,"grant should project the subject as a local_user"),z(Array.isArray(G?.groups)&&(G?.groups).includes("ecv-group-1"),"granted membership should list the target group"),z(G?.operation==="add","grant projection uses the add operation");let H=y(Y0(Y.at(-1)?.payload??{})?.local_users).find((J)=>J.unique_id==="ecv-user-2");z(H?.operation==="delete","revoke projection uses the delete operation")}},{name:"ion.authz.oaa.sync uploads a full (non-incremental) snapshot",fn:async()=>{await Q().oaa.clear();let K=await l0(Q(),$,async(G)=>G.authz.oaa.sync({users:[{id:"ecv-sync-user",userName:"ecv-sync-user@example.com",displayName:"ECV Sync User",email:"ecv-sync-user@example.com"}],groups:[{id:"ecv-sync-group",name:"ECV Sync Group",description:"ECV sync group"}]}));z(K.success===!0,"full sync should report success from the receiver");let Y=(await Q().oaa.pushes()).at(-1);z(Y!==void 0,"expected a recorded OAA sync push"),z(Y.isIncremental===!1,"a full snapshot sync must not be marked incremental");let Z=Y0(Y.payload);z(Z!==void 0,"sync payload must contain an application"),z(y(Z?.local_users).some((G)=>G.unique_id==="ecv-sync-user"),"sync should include the snapshot users"),z(y(Z?.local_groups).some((G)=>G.unique_id==="ecv-sync-group"),"sync should include the snapshot groups")}},{name:"ion.authz.oaa.sync surfaces config trusted provisioning clients with eci-trusted provenance",fn:async()=>{await Q().oaa.clear();let K={issuer:"https://idp.example/realms/ops",audience:["ecv-app"],clientId:"ecv-provisioner",permissions:[k1.id]};await l0(Q(),$,async(j)=>j.authz.oaa.sync({}),[K]);let Y=(await Q().oaa.pushes()).at(-1);z(Y!==void 0,"expected a recorded OAA sync push"),z(Y.isIncremental===!1,"trusted-client union rides the full snapshot, not an incremental push");let Z=Y0(Y.payload),G=y(Z?.local_users).find((j)=>j.unique_id==="ecv-provisioner");z(G!==void 0,"the trusted provisioning client appears as a local_user");let W=G?.custom_properties??{};z(W.source==="eci-trusted","the trusted client carries eci-trusted provenance");let J=y(Y.payload.identity_to_permissions).find((j)=>j.identity==="ecv-provisioner");z(J!==void 0,"the trusted client projects its declared permission assignment")}},{name:"stored workload grants carry the OAA workload stamp and SCIM Workload member type",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,H1.id),Y=await E(),Z=new v,G=new U;await A(Z,G,Y,{subject:"ecv-reporting-workload",subjectType:"workload",targetType:"group",target:"region_north_librarian"});let W=(await Z.listByTarget("group","region_north_librarian")).items,H=_(D5(Q(),"ecv-stored-workload-projection"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:Z,accessIndex:G,resources:Y.resources}});try{await H.ready($);let J=W[0];z(J!==void 0,"expected the stored workload grant root"),await H.authz.oaa.grant(J);let j=(await Q().oaa.pushes()).at(-1);z(j!==void 0,"expected a recorded incremental OAA grant push"),z(j.isIncremental===!0,"the workload grant uses an incremental payload");let D=y(Y0(j.payload)?.local_users).find((F)=>F.unique_id==="ecv-reporting-workload");z(D?.custom_properties?.identity_type==="workload","the incremental local_user carries identity_type=workload"),await Q().oaa.clear(),await H.authz.oaa.sync({users:[{id:"ecv-reporting-workload",userName:"ecv-reporting-workload",tenantId:"ecv-preserved-tenant"}],access:W});let P=(await Q().oaa.pushes()).at(-1);z(P!==void 0,"expected a recorded OAA sync push");let B=y(Y0(P.payload)?.local_users).find((F)=>F.unique_id==="ecv-reporting-workload");z(B!==void 0,"the stored workload grant projects its subject as a local_user");let q=B?.custom_properties??{};z(q.identity_type==="workload","the local_user carries identity_type=workload"),z(q.tenantId==="ecv-preserved-tenant","the workload stamp preserves existing local_user custom properties"),await A(Z,G,Y,{subject:"ecv-reporting-workload",subjectType:"user",targetType:"group",target:"region_north_librarian"});let L=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/region_north_librarian",{headers:{Authorization:`Bearer ${K}`}}));z(L.status===200,`requestable Group read should succeed, got ${L.status}`);let O=await L.json(),M=O[W0]??{};z(M.requestable===!0,"the SCIM group is requestable"),z(O.members?.some((F)=>F.value==="ecv-reporting-workload"&&F.type==="Workload")===!0,"the stored workload grant appears as a SCIM member with type Workload"),z(O.members?.some((F)=>F.value==="ecv-reporting-workload"&&F.type==="User")===!0,"a same-id user grant remains a distinct SCIM member")}finally{H.close?.()}}},{name:"incremental group push is referentially closed (conferred role + its permission definitions ride along)",fn:async()=>{await Q().oaa.clear();let K=new Z0;await K.permissions.set({id:"ecv-status",name:"System Status Checks",description:"status"}).ready(),await K.roles.set({id:"ecv-admin",name:"Admin",description:"admin",permissions:["ecv-status"]}).ready();let Y={id:"ecv-supers",name:"Super Admins",description:"closure test",roles:["ecv-admin"]};await K.groups.set(Y).ready(),await Q().oaa.clear(),await w5(Q(),$,K,async(j)=>{await j.authz.oaa.pushGroup(Y)});let Z=(await Q().oaa.pushes()).at(-1);z(Z!==void 0,"expected a recorded OAA group push");let G=Y0(Z.payload),W=y(G?.local_roles);z(W.some((j)=>j.unique_id==="ecv-admin"),"the conferred role definition must ride along so the reference is not dangling");let J=y(Z.payload.permissions).find((j)=>j.unique_id==="ecv-status");z(J!==void 0,"the permission the conferred role confers must also be included"),z(J?.name==="System Status Checks","the closed-over permission definition carries its display name (not just an id)")}},{name:"full sync nests Resource.resources as OAA sub_resources (team owns project)",fn:async()=>{await Q().oaa.clear();let K={id:"team:acme",type:"team",name:"Acme",description:"Acme team",resources:["project:phoenix"]},Y={id:"project:phoenix",type:"project",name:"Phoenix",description:"Phoenix project",parent:"team:acme"};await l0(Q(),$,async(j)=>{await j.authz.oaa.sync({resources:[K,Y]})});let Z=(await Q().oaa.pushes()).at(-1);z(Z!==void 0,"expected a recorded OAA sync push"),z(Z.isIncremental===!1,"full sync must not be marked incremental");let G=Y0(Z.payload),W=y(G?.resources);z(W.some((j)=>j.id==="team:acme"),"the team resource is a top-level OAA resource"),z(!W.some((j)=>j.id==="project:phoenix"),"the child project must not also appear as a top-level sibling");let H=W.find((j)=>j.id==="team:acme"),J=y(H?.sub_resources);z(J.some((j)=>j.id==="project:phoenix"),"the project nests under the team as sub_resources")}},{name:"incremental pushResource re-emits the top-level ancestor subtree with nested sub_resources",fn:async()=>{await Q().oaa.clear();let K=new Z0,Y=new F5,Z={id:"team:acme",type:"team",name:"Acme",description:"Acme team",resources:["project:phoenix"]},G={id:"project:phoenix",type:"project",name:"Phoenix",description:"Phoenix project",parent:"team:acme"};Y.set(Z),Y.set(G),await Q().oaa.clear(),await w5(Q(),$,K,async(P)=>{await P.authz.oaa.pushResource(G)},Y);let W=(await Q().oaa.pushes()).at(-1);z(W!==void 0,"expected a recorded OAA resource push"),z(W.isIncremental===!0,"pushResource is an incremental push");let H=Y0(W.payload),J=y(H?.resources);z(J.some((P)=>P.id==="team:acme"),"incremental push walks parent to the top-level ancestor"),z(!J.some((P)=>P.id==="project:phoenix"),"the child is nested, not duplicated at top level");let j=J.find((P)=>P.id==="team:acme"),D=y(j?.sub_resources);z(D.some((P)=>P.id==="project:phoenix"),"the pushed project appears under the ancestor as sub_resources")}},{name:"ion.authz.oaa entity deletes emit incremental delete operations for each entity kind",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(Z)=>{await Z.authz.oaa.deleteUser("ecv-del-user"),await Z.authz.oaa.deleteGroup("ecv-del-group"),await Z.authz.oaa.deleteRole("ecv-del-role"),await Z.authz.oaa.deletePermission("ecv-del-perm"),await Z.authz.oaa.deleteResource("ecv-del-resource")});let K=await Q().oaa.pushes();z(K.length>=5,"each entity delete should produce a push");let Y=(Z,G,W)=>K.some((H)=>{let J=Y0(H.payload)??{};return Z(J,H.payload).some((j)=>j[G]===W&&j.operation==="delete")});z(Y((Z)=>y(Z.local_users),"unique_id","ecv-del-user"),"deleteUser marks the user with operation:delete"),z(Y((Z)=>y(Z.local_groups),"unique_id","ecv-del-group"),"deleteGroup marks the group with operation:delete"),z(Y((Z)=>y(Z.local_roles),"unique_id","ecv-del-role"),"deleteRole marks the role with operation:delete"),z(Y((Z,G)=>y(G.permissions),"unique_id","ecv-del-perm"),"deletePermission marks the permission with operation:delete"),z(Y((Z)=>y(Z.resources),"id","ecv-del-resource"),"deleteResource marks the resource with operation:delete")}},{name:"OAA full-sync endpoint returns 404 when RemoteConfig.authz.oaa.fullSync is disabled",fn:async()=>{let K=_(C1(Q(),!1),{validators:R(),stores:{catalogStore:new Z0}});try{await K.ready($);let Y=await K.handler(new Request(O1,{method:"POST"}));z(Y.status===404,`disabled full-sync endpoint should 404, got ${Y.status}`)}finally{K.close?.()}}},{name:"OAA full-sync endpoint requires workload auth when enabled (401 without a token)",fn:async()=>{let K=_(C1(Q(),!0),{validators:R(),stores:{catalogStore:new Z0}});try{await K.ready($);let Y=await K.handler(new Request(O1,{method:"POST"}));z(Y.status!==404,"an enabled endpoint must not 404"),z(Y.status!==200,"an unauthenticated caller must not be allowed to trigger a sync"),z(Y.status===401||Y.status===403,`unauthenticated full-sync should be denied (401/403), got ${Y.status}`)}finally{K.close?.()}}},{name:"OAA full-sync endpoint runs a snapshot for an authorized workload and returns 405 for non-POST",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,j5.id),Y=_(C1(Q(),!0),{validators:R(),stores:{catalogStore:new Z0}});try{await Y.ready($);let Z=await Y.handler(new Request(O1,{method:"GET"}));z(Z.status===405,`full-sync endpoint is POST-only, got ${Z.status} for GET`);let G=await Y.handler(new Request(O1,{method:"POST",headers:{Authorization:`Bearer ${K}`}}));z(G.status===200,`authorized full-sync should succeed, got ${G.status}`);let W=await G.json();z(W.reconcile!==void 0&&typeof W.reconcile.subjects==="number"&&typeof W.reconcile.roots==="number"&&typeof W.reconcile.cleared==="number","the endpoint bundles a full-reconcile report alongside the OAA sync result");let H=(await Q().oaa.pushes()).at(-1);z(H!==void 0,"the endpoint should drive an OAA sync push"),z(H.isIncremental===!1,"the endpoint runs a full (snapshot) sync, not an incremental push")}finally{Y.close?.()}}},{name:"routing.resolve replacing iam.users inherits the requiredPermission gate unless it opts out with null (§16.1)",fn:async()=>{let K=await E(),Y="custom-iam-users-handler",Z=_(h({id:"ecv-resolve-inherit",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:K.catalog,accessStore:new v,accessIndex:new U,resources:K.resources},routing:{resolve:(G,W,H)=>{if(!H||new URL(G.url).pathname!=="/api/ionite/iam/Users")return H;let J={module:H.module,kind:"iam.users.custom",handler:async()=>new Response("custom-iam-users-handler",{status:200})};if(G.headers.get("x-ion-resolve-mode")==="public")return{...J,requiredPermission:null};return J}}});try{await Z.ready($);let G=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Users",{headers:{"x-ion-resolve-mode":"inherit"}}));z(G.status===401||G.status===403,`a replaced route omitting requiredPermission must inherit the gate (401/403), got ${G.status}`),z(await G.text()!=="custom-iam-users-handler","the custom handler must not run when the inherited gate denies");let W=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Users",{headers:{"x-ion-resolve-mode":"public"}}));z(W.status===200,`requiredPermission: null must reach the handler, got ${W.status}`),z(await W.text()==="custom-iam-users-handler","the explicit public opt-out reaches the custom handler");let H=await Z.handler(new Request("https://ecv.test/api/ionite/does-not-exist"));z(H.status===404,`an unmatched route returns 404, got ${H.status}`);let J=await H.json();w(J.error,"not_found",'the router 404 uses the unified JSON shape { error: "not_found" }')}finally{Z.close?.()}}},{name:"a failed SCIM Group membership mutation is auto-repaired by the SDK (awaited subject reconcile within the request, no IDP retry)",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=await E(),Z=new v,G=new H2,W=_(h({id:"ecv-scim-repair",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:Z,accessIndex:G,resources:Y.resources}});try{await W.ready($);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"repair-user"}]}]})}));z(H.status>=400,`a failed access mutation should return a SCIM error, got ${H.status}`),z((await Z.listBySubject("repair-user","user")).items.length===1,"the grant root is durably committed even though the index write failed"),z((await G.hasGroup("repair-user","staff",void 0,"user")).allow,"the SDK awaited-repair re-materializes the subject index within the request (no IDP retry)"),z((await G.hasPermission("repair-user","read",void 0,"user")).allow,"the repaired closure includes the role/permission edges (editor -> read via staff)")}finally{W.close?.()}}},{name:"settlement: onAccessMutation fires exactly once with summary.egress on every exit path",fn:async()=>{let K=Q().baseUrl,Y=await S(K,V0.id),Z=await S(K,x9.id),G={jwksUri:`${K}/workload/certs`,audience:`${K}/workload`},W=(H,J)=>new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${J}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:H}]}]})});{let H=await E(),J=[],j=_(h({id:"settle-success",workload:G,authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),authz:{onAccessMutation:(D)=>J.push(D)},stores:{catalogStore:H.catalog,accessStore:new v,accessIndex:new U,resources:H.resources}});try{await j.ready($);let D=await j.handler(W("settle-ok",Y));z(D.status>=200&&D.status<300,`success exit should be 2xx, got ${D.status}`),w(J.length,1,"onAccessMutation fires exactly once on the success exit"),z(J[0]?.egress!==void 0,"summary.egress is populated on the success exit"),z(J[0]?.access.some((P)=>P.status==="applied"),"success summary records the applied grant")}finally{j.close?.()}}{let H=await E(),J=[],j=_(h({id:"settle-access-fail",workload:G,authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),authz:{onAccessMutation:(D)=>J.push(D)},stores:{catalogStore:H.catalog,accessStore:new v,accessIndex:new H2,resources:H.resources}});try{await j.ready($);let D=await j.handler(W("settle-fail",Y));z(D.status>=400,`access-failure exit should return a SCIM error, got ${D.status}`),w(J.length,1,"onAccessMutation fires exactly once on the access-failure exit"),z(J[0]?.egress!==void 0,"summary.egress is populated on the access-failure exit"),z(J[0]?.access.some((P)=>P.status==="failed"),"access-failure summary records the failed access mutation")}finally{j.close?.()}}{let H=[],J=_(h({id:"settle-metadata-fail",workload:G,authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),authz:{onAccessMutation:(j)=>H.push(j)},stores:{userStore:new R9}});try{await J.ready($);let j=await J.handler(new Request("https://ecv.test/api/ionite/iam/Users",{method:"POST",headers:{Authorization:`Bearer ${Z}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],userName:"settle-metadata-fail@example.com",externalId:"settle-metadata-fail",name:{givenName:"Settle",familyName:"Fail"},emails:[{value:"settle-metadata-fail@example.com",primary:!0}]})}));z(j.status>=400,`metadata-failure exit should return a SCIM error, got ${j.status}`),w(H.length,1,"onAccessMutation fires exactly once on the metadata-failure exit"),z(H[0]?.egress!==void 0,"summary.egress is populated on the metadata-failure exit"),z(H[0]?.metadataApplied===!1,"metadata-failure summary marks metadataApplied false")}finally{J.close?.()}}}},{name:"settlement: a degraded OAA push is non-fatal (SCIM stays 2xx, summary.egress.oaa is degraded)",fn:async()=>{let K=Q(),Y=K.baseUrl,Z=await S(Y,V0.id),G=await E(),W=[],H=_(h({id:"settle-oaa-degraded",workload:{jwksUri:`${Y}/workload/certs`,audience:`${Y}/workload`},authz:{scopesAsEntitlements:!0,oaa:{url:`${Y}/__oaa_missing__`,apiKey:K.oaa.apiKey,providerId:K.oaa.providerId,datasourceId:K.oaa.datasourceId,applicationName:"ECV Settlement Degraded"}},iam:{}}),{validators:R(),authz:{onAccessMutation:(J)=>W.push(J)},stores:{catalogStore:G.catalog,accessStore:new v,accessIndex:new U,resources:G.resources}});try{await H.ready($),z(H.authz.oaa.isReady(),"OAA must be configured/ready so the push is actually attempted");let J=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${Z}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"oaa-degraded-member"}]}]})}));z(J.status>=200&&J.status<300,`a degraded OAA push must not fail the SCIM response, got ${J.status}`),w(W.length,1,"onAccessMutation fires exactly once"),w(W[0]?.egress?.oaa.status,"degraded","the failed OAA push is recorded as degraded egress"),z(W[0]?.access.some((j)=>j.status==="applied"),"the underlying store mutation still committed (applied)")}finally{H.close?.()}}},{name:"SCIM Groups exposes only requestable app rows across list/get/externalId and write paths",fn:async()=>{let K=await S(Q().baseUrl,H1.id),Y=await S(Q().baseUrl,V0.id),Z=await E();await Z.catalog.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,roles:["editor"],resources:["team:acme"],source:"app"}).ready();let G=A1(),W=_(e0(Q().baseUrl,"ecv-app-row-read"),{validators:R(),stores:{catalogStore:Z.catalog,accessStore:new v,accessIndex:new U,resources:G}});try{await W.ready($);let H={Authorization:`Bearer ${K}`},J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{headers:H}));z(J.status===200,`Groups list should succeed, got ${J.status}`);let j=await J.json(),D=(j.Resources??[]).map((C)=>C.id);z(D.includes("acme_team_admin"),"the durable app-authored entitlement is listed"),z(!D.includes("internal_staff"),"the non-requestable stored group is hidden"),z(j.totalResults===D.length,"totalResults counts only the requestable IAM surface");let q=((await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq true")}`,{headers:H}))).json()).Resources??[]).map((C)=>C.id);z(q.includes("acme_team_admin"),"the app entitlement passes the requestable filter"),z(!q.includes("internal_staff"),"the non-requestable stored group is excluded");let O=await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq false")}`,{headers:H}))).json();z(O.totalResults===0&&(O.Resources??[]).length===0,"requestable eq false cannot reveal internal definitions");let F=await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "iam-acme-team-admin"')}`,{headers:H}))).json();z((F.Resources??[]).some((C)=>C.id==="acme_team_admin"),"externalId filtering matches the row by providerId");let x=await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "iam-internal-staff"')}`,{headers:H}))).json();z(x.totalResults===0&&(x.Resources??[]).length===0,"externalId lookup cannot reveal a non-requestable row");let b=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{headers:H}));z(b.status===200,`app Group GET should succeed, got ${b.status}`);let p=await b.json();z(p.id==="acme_team_admin"&&p.displayName==="Acme Admin","durable presentation is projected"),z(p[W0]?.source==="app","the SCIM projection carries source:app provenance"),z(p[W0]?.resources?.[0]==="team:acme","the SCIM projection carries the durable resource binding");let F0=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/internal_staff",{headers:H}));z(F0.status===404,`non-requestable point GET must be 404, got ${F0.status}`);let k=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/internal_staff",{method:"PATCH",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"must-not-grant"}]}]})}));z(k.status===404,`IAM membership write on an internal entitlement must be 404, got ${k.status}`)}finally{W.close?.()}}},{name:"a member grant on an app catalog row materializes resource-scoped access and IAM receipt write-back is accepted",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=await E();await Y.catalog.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,roles:["editor"],resources:["team:acme"],source:"app"}).ready();let Z=A1(),G=new v,W=new U,H=_(e0(Q().baseUrl,"ecv-app-row-write"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:G,accessIndex:W,resources:Z}});try{await H.ready($);let J={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},j=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"derived-member",type:"User"},{value:"receipt-workload",type:"Workload"}]}]})}));z(j.status<300,`member add on an app entitlement should succeed, got ${j.status}`),z((await W.hasRole("derived-member","editor","team:acme","user")).allow,"the grant confers the app-authored role scoped to the backing resource"),z(!(await W.hasRole("derived-member","editor","region:north","user")).allow,"the app-authored scope does not leak to unrelated resources");let D=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.providerId`,value:"iam-group-42"},{op:"replace",path:`${W0}.requestUrl`,value:"https://iam.example.test/request/42"}]})}));z(D.status<300,`IAM receipt PATCH should succeed, got ${D.status}`);let P=await Y.catalog.groups.get("acme_team_admin");z(P?.providerId==="iam-group-42","IAM providerId is written back to the app row"),z(P?.requestUrl==="https://iam.example.test/request/42","IAM requestUrl is written back to the app row")}finally{H.close?.()}}},{name:"code templates union publicly, materialize on SCIM discovery, and preserve IAM-owned presentation",fn:async()=>{let K=await S(Q().baseUrl,H1.id),Y=await S(Q().baseUrl,V0.id),Z=await E(),G=new U;await Z.catalog.groups.set({id:"code_staff",name:"IAM Renamed Staff",displayName:"IAM Renamed Staff",description:"Presentation owned by IAM",requestable:!1,source:"code",providerId:"provider-code-staff",requestUrl:"https://iam.example.test/request/code-staff",roles:["stored-role-must-not-control-structure"]}).ready();let W=_(e0(Q().baseUrl,"ecv-code-read"),{validators:R(),stores:{catalog:J1(),catalogStore:Z.catalog,accessStore:new v,accessIndex:G}});try{await W.ready($);let H={Authorization:`Bearer ${K}`},j=(await W.authz.entitlements.list()).items.map((k)=>k.id);z(j.includes("code_staff"),"the public API includes a code id with a materialized row"),z(j.includes("code_editor"),"the public API includes unmaterialized code templates"),z(j.includes("staff"),"the public API unions app/store rows"),z(await Z.catalog.roles.get("code_editor")===void 0,"public union reads do not themselves materialize code templates");let D=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{headers:H}));z(D.status===200,`Groups list should succeed, got ${D.status}`);let P=await D.json(),B=(P.Resources??[]).map((k)=>k.id);z(B.includes("code_staff"),"the code-declared group is listed"),z(!B.includes("code_editor"),"the non-requestable code role is hidden from IAM"),z(B.includes("staff"),"the requestable store group remains visible to IAM"),z(!B.includes("internal_staff"),"the non-requestable store group is hidden from IAM"),z(P.totalResults===B.length,"totalResults counts only requestable entries"),z((await Z.catalog.groups.get("code_staff"))?.source==="code","SCIM Group list lazily materializes requestable code templates"),z(await Z.catalog.roles.get("code_editor")===void 0&&await Z.catalog.permissions.get("code_read")===void 0,"SCIM Group list does not materialize internal code templates");let O=((await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq true")}`,{headers:H}))).json()).Resources??[]).map((k)=>k.id);z(O.includes("code_staff"),"the code template requestability wins over the stale stored presentation"),z(O.includes("staff"),"the requestable store group remains included"),z(!O.includes("internal_staff"),"the non-requestable store group is excluded");let T=((await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "code_editor"')}`,{headers:H}))).json()).Resources??[]).find((k)=>k.id==="code_editor");z(T===void 0,"externalId filtering hides the internal code entitlement"),z(await Z.catalog.roles.get("code_editor")===void 0,"hidden externalId lookup does not materialize the internal code entitlement");let x=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{headers:H}));z(x.status===200,`code Group GET should succeed, got ${x.status}`);let b=await x.json();z(b.id==="code_staff"&&b.externalId==="provider-code-staff","the App id and IAM provider id are projected in their SCIM id/externalId slots"),z(b.displayName==="IAM Renamed Staff","the existing row owns presentation"),z(b[W0]?.requestable===!0,"code-owned requestability overrides the stale stored value"),z(b[W0]?.providerId==="provider-code-staff"&&b[W0]?.requestUrl==="https://iam.example.test/request/code-staff","the existing row owns IAM receipts");let p=await Z.catalog.groups.get("code_staff");z(p?.displayName==="IAM Renamed Staff"&&p.roles?.[0]==="stored-role-must-not-control-structure","insert-if-absent discovery never overwrites an existing durable row");let F0=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"iam-presentation-member"}]}]})}));z(F0.status<300,`membership grant should succeed, got ${F0.status}`),z((await G.hasRole("iam-presentation-member","code_editor",void 0,"user")).allow,"code structural references remain authoritative over conflicting stored structure"),z(!(await G.hasRole("iam-presentation-member","stored-role-must-not-control-structure",void 0,"user")).allow,"the IAM-owned row cannot replace code-owned graph structure")}finally{W.close?.()}}},{name:"first code membership grant materializes durably; IAM presentation writes succeed while structure stays code-owned",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new v,Z=new U,G=new Z0,W=_(e0(Q().baseUrl,"ecv-code-write"),{validators:R(),stores:{catalog:J1(),catalogStore:G,accessStore:Y,accessIndex:Z}});try{await W.ready($);let H={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"};z(await G.groups.get("code_staff")===void 0,"the code template begins unmaterialized");let J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"code-member"}]}]})}));z(J.status<300,`member add on a code entitlement should succeed, got ${J.status}`);let j=await G.groups.get("code_staff");z(j?.source==="code","the first membership grant materializes source:code"),z(j?.roles?.[0]==="code_editor","the durable candidate initially carries code structure"),z((await Z.hasRole("code-member","code_editor",void 0,"user")).allow,"the grant confers the code role"),z((await Z.hasPermission("code-member","code_read",void 0,"user")).allow,"the code role confers its permissions");let D=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"remove",path:'members[value eq "code-member"]'}]})}));z(D.status<300,`member remove should succeed, got ${D.status}`),z(!(await Z.hasGroup("code-member","code_staff",void 0,"user")).allow,"the revoke converges the closure");let P=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:"IAM Renamed Code Staff"},{op:"replace",path:`${W0}.providerId`,value:"iam-code-staff"},{op:"replace",path:`${W0}.requestUrl`,value:"https://iam.example.test/request/code-staff"}]})}));z(P.status<300,`code presentation/receipt PATCH should succeed, got ${P.status}`);let B=await G.groups.get("code_staff");z(B?.displayName==="IAM Renamed Code Staff","IAM presentation persists on the row"),z(B?.providerId==="iam-code-staff"&&B.requestUrl==="https://iam.example.test/request/code-staff","IAM receipt fields persist on the row"),z(B?.roles?.[0]==="code_editor","presentation write-back does not erase code structure");let q=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.requestable`,value:!1}]})}));z(q.status===403,`code-owned requestability PATCH must be 403, got ${q.status}`);let L=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.resources`,value:["team:hijack"]}]})}));z(L.status===403,`code structure PATCH must be 403, got ${L.status}`);let O=await L.json();z(O.scimType==="mutability","the rejection is a SCIM mutability error");let M=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PUT",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],id:"code_staff",externalId:"iam-code-staff-v2",displayName:"Code Staff"})}));z(M.status<300,`provider externalId PUT should succeed, got ${M.status}`),z((await G.groups.get("code_staff"))?.providerId==="iam-code-staff-v2","the inbound client externalId is stored as providerId without changing the App-owned catalog id");let F=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"DELETE",headers:H}));z(F.status===403,`active code Group DELETE must be 403, got ${F.status}`)}finally{W.close?.()}}},{name:"SCIM create stores the client externalId as providerId without claiming the App id",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0,Z=_(e0(Q().baseUrl,"ecv-code-collision"),{validators:R(),stores:{catalog:J1(),catalogStore:Y,accessStore:new v,accessIndex:new U}});try{await Z.ready($);let G=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"Impostor",externalId:"code_staff"})}));z(G.status===201,`a client externalId create must succeed, got ${G.status}`);let W=await G.json();z(W.id!=="code_staff","the App mints a distinct catalog id"),z(W.externalId==="code_staff","the response echoes the client-owned externalId"),z(W.id!==void 0&&(await Y.groups.get(W.id))?.providerId==="code_staff","the client externalId is stored as providerId")}finally{Z.close?.()}}},{name:"SCIM Group create defaults to requestable and rejects explicitly internal definitions",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0,Z=_(e0(Q().baseUrl,"ecv-requestable-create"),{validators:R(),stores:{catalogStore:Y,accessStore:new v,accessIndex:new U}});try{await Z.ready($);let G={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},W=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:G,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],externalId:"iam-authored-requestable",displayName:"IAM Authored Requestable"})}));z(W.status===201,`plain SCIM Group create should succeed, got ${W.status}`);let H=await W.json();z(H[W0]?.requestable===!0,"a SCIM-authored Group defaults to requestable"),z((await Y.groups.get(H.id??""))?.requestable===!0,"the requestable default is durable");let J=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:G,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group",W0],externalId:"iam-authored-internal",displayName:"IAM Authored Internal",[W0]:{requestable:!1}})}));z(J.status===400,`IAM cannot create an internal entitlement, got ${J.status}`)}finally{Z.close?.()}}},{name:"full reconcile deprecates retired code rows; SCIM exposes retirement and only then permits deletion",fn:async()=>{let K=await S(Q().baseUrl,H1.id),Y=await S(Q().baseUrl,V0.id),Z=new Z0;await Z.groups.set({id:"retired_code_group",providerId:"iam-retired-code-group",name:"Retired Code Group",description:"No longer present in the current CodeCatalog",requestable:!0,source:"code",deprecated:!1}).ready(),await Z.groups.set({id:"pull_retired_code_group",providerId:"iam-pull-retired-code-group",name:"Pull-Retired Code Group",description:"Retired lazily during SCIM discovery",requestable:!0,source:"code",deprecated:!1}).ready();let G=_(e0(Q().baseUrl,"ecv-code-retirement"),{validators:R(),stores:{catalog:J1(),catalogStore:Z,accessStore:new v,accessIndex:new U}});try{await G.ready($);let W=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/pull_retired_code_group",{headers:{Authorization:`Bearer ${K}`}}));z(W.status===200,"SCIM GET should project a retired code row"),z((await Z.groups.get("pull_retired_code_group"))?.deprecated===!0,"SCIM discovery lazily marks a source:code row missing from the current CodeCatalog deprecated");let H=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/pull_retired_code_group",{method:"PATCH",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.requestable`,value:!1}]})}));z(H.status===403,`retired source:code requestability remains immutable, got ${H.status}`);let J=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{method:"DELETE",headers:{Authorization:`Bearer ${Y}`}}));z(J.status===403,"a source:code row cannot be deleted before retirement"),await G.authz.reconcile(),z((await Z.groups.get("retired_code_group"))?.deprecated===!0,"full reconcile marks a source:code row missing from the current CodeCatalog deprecated");let j=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{headers:{Authorization:`Bearer ${K}`}}));z(j.status===200,`deprecated Group GET should succeed, got ${j.status}`);let D=await j.json();z(D[W0]?.source==="code"&&D[W0]?.deprecated===!0,"SCIM projects source:code and deprecated:true");let P=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{method:"DELETE",headers:{Authorization:`Bearer ${Y}`}}));z(P.status===204,`a deprecated code row may be deleted, got ${P.status}`),z(await Z.groups.get("retired_code_group")===void 0,"the retired row is removed")}finally{G.close?.()}}},{name:"OAA full sync unions code templates with durable app rows and resource data",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,j5.id),Y=new Z0;await Y.groups.set({id:"stored-group",name:"Stored Group",description:"stored",source:"app"}).ready(),await Y.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,roles:["code_editor"],resources:["team:acme"],source:"app"}).ready();let Z=_(C1(Q(),!0),{validators:R(),stores:{catalog:J1(),catalogStore:Y,resources:A1()}});try{await Z.ready($);let G=await Z.handler(new Request(O1,{method:"POST",headers:{Authorization:`Bearer ${K}`}}));z(G.status===200,`authorized full-sync should succeed, got ${G.status}`);let W=(await Q().oaa.pushes()).at(-1);z(W!==void 0&&W.isIncremental===!1,"the endpoint runs a full (snapshot) sync");let H=Y0(W.payload),J=y(H?.local_groups).map((P)=>P.unique_id);z(J.includes("code_staff"),"code-declared groups are unioned into the projection"),z(J.includes("stored-group"),"store catalog groups remain in the projection"),z(J.includes("acme_team_admin"),"durable resource-scoped app rows are projected");let j=y(H?.local_roles).map((P)=>P.unique_id);z(j.includes("code_editor"),"code-declared roles are unioned into the projection");let D=y(W.payload.permissions).map((P)=>P.unique_id);z(D.includes("code_read"),"code-declared permissions are unioned into the projection")}finally{Z.close?.()}}},{name:"ResourceProvider.changed triggers an off-request-path reconcile and an incremental OAA push",fn:async()=>{await Q().oaa.clear();let K=await E(),Y=new v,Z=new U;K.resources.set({id:"team:a",type:"team",name:"Team A",description:"A team"}),await A(Y,Z,K,{subject:"lead",subjectType:"user",targetType:"permission",target:"manage",resources:["team:a"]});let G=_(L5(Q()),{validators:R(),stores:{catalogStore:K.catalog,accessStore:Y,accessIndex:Z,resources:K.resources}});try{await G.ready($),z(!(await Z.hasPermission("lead","manage","proj:a1","user")).allow,"no child access before the change"),await Q().oaa.clear(),K.resources.set({id:"proj:a1",type:"project",name:"Project A1",description:"A1",parent:"team:a"});let W=!1;for(let J=0;J<100&&!W;J++)await new Promise((j)=>setTimeout(j,10)),W=(await Z.hasPermission("lead","manage","proj:a1","user")).allow;z(W,"the changed signal drives a reconcile that cascades manage to the new child");let H=!1;for(let J=0;J<100&&!H;J++)await new Promise((j)=>setTimeout(j,10)),H=(await Q().oaa.pushes()).some((j)=>{let D=Y0(j.payload),P=(B)=>B.flatMap((q)=>[q,...P(y(q.sub_resources))]);return j.isIncremental===!0&&P(y(D?.resources)).some((B)=>B.id==="proj:a1")});z(H,"the changed signal drives an incremental OAA resource push")}finally{G.close?.()}}},{name:"OAA full sync includes ResourceProvider resources and durable app-authored catalog rows",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,j5.id),Y=new Z0;await Y.groups.set({id:"stored-group",name:"Stored Group",description:"stored",source:"app"}).ready(),await Y.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,resources:["team:acme"],source:"app"}).ready();let Z=_(C1(Q(),!0),{validators:R(),stores:{catalogStore:Y,resources:A1()}});try{await Z.ready($);let G=await Z.handler(new Request(O1,{method:"POST",headers:{Authorization:`Bearer ${K}`}}));z(G.status===200,`authorized full-sync should succeed, got ${G.status}`);let W=(await Q().oaa.pushes()).at(-1);z(W!==void 0,"the endpoint should drive an OAA sync push"),z(W.isIncremental===!1,"the endpoint runs a full (snapshot) sync");let H=Y0(W.payload);z(y(H?.resources).some((j)=>j.id==="team:acme"),"the snapshot streams resources from the ResourceProvider");let J=y(H?.local_groups).map((j)=>j.unique_id);z(J.includes("acme_team_admin"),"durable app catalog rows are included in the projection"),z(J.includes("stored-group"),"stored catalog groups remain in the projection")}finally{Z.close?.()}}},{name:"the SDK write path (SCIM access mutation + ResourceProvider.changed) emits only incremental OAA pushes, never a full snapshot",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,V0.id),Y=await E(),Z=A1(),G=new v,W=new U,H=_(D5(Q(),"ecv-scim-oaa-guard"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:G,accessIndex:W,resources:Z}});try{await H.ready($),z(H.authz.oaa.isReady(),"OAA must be ready so the write path actually projects");let J={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"};await Y.catalog.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],resources:["team:acme"],source:"app"}).ready();let j=async()=>(await Q().oaa.pushes()).every((L)=>L.isIncremental===!0),D=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"guard-user-1",type:"User"}]}]})}));z(D.status<300,`stored group member add should succeed, got ${D.status}`);let P=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"guard-user-2",type:"User"}]}]})}));z(P.status<300,`app group member add should succeed, got ${P.status}`),z((await Q().oaa.pushes()).length>=2,"each SCIM membership grant should project an incremental push"),z((await Q().oaa.pushes()).some((L)=>{let O=Y0(L.payload);return L.isIncremental===!0&&y(O?.local_groups).some((M)=>M.unique_id==="acme_team_admin")}),"the durable app catalog write projects the Group incrementally"),z(await j(),"a SCIM access mutation must not trigger a full OAA snapshot sync"),Z.set({id:"team:beta",type:"team",name:"Beta",description:"Beta team"});let B=!1;for(let L=0;L<100&&!B;L++)await new Promise((O)=>setTimeout(O,10)),B=(await Q().oaa.pushes()).some((O)=>{let M=Y0(O.payload);return O.isIncremental===!0&&y(M?.resources).some((F)=>F.id==="team:beta")});z(B,"a resource change should project the resource to OAA incrementally"),await new Promise((L)=>setTimeout(L,50));let q=(await Q().oaa.pushes()).filter((L)=>L.isIncremental===!1);z(q.length===0,`the SDK write path emitted ${q.length} full snapshot push(es); full sync must only run via the Ops-gated endpoint`)}finally{H.close?.()}}},{name:"§7.1 pushAccessChanges batches N membership changes into ONE incremental push (O(1), not O(N))",fn:async()=>{await Q().oaa.clear();let K=Array.from({length:100},(J,j)=>({change:{subject:`batch-user-${j}`,subjectType:"user",targetType:"group",target:"batch-group"},op:"add"})),Y=new Z0;await Y.groups.set({id:"batch-group",name:"Batch Group",description:"Batch projection target"}).ready(),await w5(Q(),$,Y,async(J)=>{await J.authz.oaa.pushAccessChanges(K)});let Z=await Q().oaa.pushes();w(Z.length,1,"a 100-change batch must issue exactly one OAA push, not one per change");let G=Z[0];z(G!==void 0,"expected the single batched push to be recorded"),z(G?.isIncremental===!0,"a batched membership push is incremental, never a full snapshot");let W=y(Y0(G?.payload??{})?.local_users);for(let J=0;J<100;J+=1){let j=W.find((D)=>D.unique_id===`batch-user-${J}`);z(j!==void 0,`subject batch-user-${J} must appear in the single batched payload`),z(Array.isArray(j?.groups)&&(j?.groups).includes("batch-group"),`subject batch-user-${J} must carry the granted group membership`)}let H=W.find((J)=>J.unique_id==="batch-user-0");z(H?.operation==="add","a batched add projects each membership with the add operation")}},{name:"§7.1 pushAccessChanges([]) is a no-op — an empty batch never contacts the OAA receiver",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(K)=>{await K.authz.oaa.pushAccessChanges([])}),w((await Q().oaa.pushes()).length,0,"an empty change batch must not issue any OAA push")}},{name:"re-pushing the SAME SCIM membership grant keeps IAM entitlement members and OAA edges stable (idempotent, no accumulation)",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,`${V0.id} ${H1.id}`),Y=await E(),Z=new v,G=new U,W=_(D5(Q(),"ecv-scim-oaa-idempotent"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:Z,accessIndex:G}});try{await W.ready($),z(W.authz.oaa.isReady(),"OAA must be ready so the write path projects each grant");let H={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},J=()=>new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"idem-user"}]}]})}),j=async()=>{let p=new Set;for(let F0 of await Q().oaa.pushes())for(let k of y(Y0(F0.payload)?.local_users)){if(k.operation==="delete")continue;let C=typeof k.unique_id==="string"?k.unique_id:"";for(let X0 of Array.isArray(k.groups)?k.groups:[])if(typeof X0==="string")p.add(`${C}:member_of:group:${X0}`)}return p},D=await W.handler(J());z(D.status<300,`first membership grant should succeed, got ${D.status}`);let P=(await Z.listByTarget("group","staff")).items.length,B=await j(),q=(await Q().oaa.pushes()).length;z(P===1,"the first grant persists exactly one membership root"),z(B.has("idem-user:member_of:group:staff"),"the first grant projects the membership edge"),z((await G.hasPermission("idem-user","read",void 0,"user")).allow,"the granted staff membership confers the editor role closure (read)");let L=await W.handler(J());z(L.status<300,`re-granting the same membership should still succeed, got ${L.status}`);let O=(await Z.listByTarget("group","staff")).items.length,M=await j(),F=(await Q().oaa.pushes()).length;z(O===P,`re-pushing the same grant must not add IAM entitlement roots (was ${P}, now ${O})`),z(M.size===B.size,`re-pushing the same grant must not add OAA edges (was ${B.size}, now ${M.size})`);for(let p of B)z(M.has(p),`OAA edge ${p} must remain stable across an identical re-push`);z(F>q,"the SDK re-projects on the second grant (the receiver, not the SDK, dedupes the edge)");let T=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{headers:{Authorization:`Bearer ${K}`}}));z(T.status===200,`Groups/staff read should succeed, got ${T.status}`);let b=((await T.json()).members??[]).filter((p)=>p.value==="idem-user");z(b.length===1,`the re-granted member must appear exactly once in the SCIM projection, saw ${b.length}`)}finally{W.close?.()}}}]}function A1(){let X=new F5;return X.set({id:"team:acme",type:"team",name:"Acme",description:"Acme team"}),X}function e0(X,$){return h({id:$,workload:{jwksUri:`${X}/workload/certs`,audience:`${X}/workload`},authz:{scopesAsEntitlements:!0},iam:{}})}async function S(X,$){let Q=new URLSearchParams({grant_type:"client_credentials",client_id:"local-workload-client",client_secret:"local-workload-secret",scope:$}),Y=await(await fetch(`${X}/workload/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:Q})).json();return z(typeof Y.access_token==="string","mock workload token endpoint must return an access_token"),Y.access_token}function S1(X,$){return _(h({id:"ecv-workload-scope",workload:{jwksUri:`${X}/workload/certs`,audience:`${X}/workload`},...$?{authz:$}:{}}),{validators:R()})}function M9(X,$,Q){return _(h({id:"ecv-workload-incoming",workload:{incoming:$},...Q?{authz:Q}:{}}),{validators:R()})}function s0(X){return new Request("https://example.test/api/task",{headers:{Authorization:`Bearer ${X}`}})}async function B5(X,$,Q){try{await X()}catch(K){let Y=K instanceof Error?K.message:String(K);if(!Y.includes($))throw Error(`${Q}: expected error containing "${$}", got "${Y}"`);return}throw Error(`${Q}: expected rejection, but the call resolved`)}function U9(X){let $=X.timeout??5000,Q=()=>v9(X.ecv);return[{name:"hasScope* authorize a workload from its token scope claim (allow / deny + 403 mapping)",fn:async()=>{let K=await S(Q().baseUrl,"oaa:sync reports:read"),Y=S1(Q().baseUrl);try{await Y.ready($);let Z=s0(K),G=await q5(Z,Y,"oaa:sync","workload");z(G.allow,"token scope oaa:sync satisfies hasScope"),z(G.subjectType==="workload","the matched principal is the workload"),z(G.subject?.id==="local-workload-client","workload id is the client_id");let W=await q5(Z,Y,"admin:all","workload");z(!W.allow&&W.reason==="forbidden","a missing scope is forbidden"),z(W.response().status===403,"forbidden scope -> 403"),z((await T9(Z,Y,["nope","reports:read"],"workload")).allow,"hasAnyScope matches one"),z(!(await P5(Z,Y,["oaa:sync","nope"],"workload")).allow,"hasAllScope needs all"),z((await P5(Z,Y,["oaa:sync","reports:read"],"workload")).allow,"hasAllScope all present")}finally{Y.close?.()}}},{name:"scopesAsEntitlements gates whether has* auto-honors a workload token scope (app-wide only)",fn:async()=>{let K=await S(Q().baseUrl,"run_oaa_sync"),Y=s0(K),Z=S1(Q().baseUrl,{scopesAsEntitlements:!1});try{await Z.ready($);let W=await _0(Y,Z,"run_oaa_sync",void 0,"workload");z(!W.allow,"scopes do not satisfy has* when scopesAsEntitlements is false")}finally{Z.close?.()}let G=S1(Q().baseUrl,{scopesAsEntitlements:!0});try{await G.ready($);let W=await _0(Y,G,"run_oaa_sync",void 0,"workload");z(W.allow,"app-wide has* auto-honors the matching token scope when enabled");let H=await _0(Y,G,"run_oaa_sync","project-a","workload");z(!H.allow,"a token scope cannot satisfy a resource-scoped has* check")}finally{G.close?.()}}},{name:"trustedProvisioningClients overlay authorizes a matching workload without a store grant or scope",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,""),G={issuer:Y,audience:[Y],clientId:"local-workload-client",permissions:[k1.id]},W=S1(K,{trustedProvisioningClients:[G]});try{await W.ready($);let H=s0(Z),J=await _0(H,W,k1.id,void 0,"workload");z(J.allow,"trusted client is authorized for its declared permission via the overlay"),z(J.decision?.metadata?.reason==="trusted-client","allow reason is trusted-client");let j=await _0(H,W,"ion:iam:read",void 0,"workload");z(!j.allow,"a permission outside the trusted client declaration is denied")}finally{W.close?.()}}},{name:"trustedProvisioningClients overlay is audience-bound (a mismatched audience does not authorize)",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,""),G={issuer:Y,audience:["https://some-other-app.example/api"],clientId:"local-workload-client",permissions:[k1.id]},W=S1(K,{trustedProvisioningClients:[G]});try{await W.ready($);let H=await _0(s0(Z),W,k1.id,void 0,"workload");z(!H.allow,"audience mismatch means the trusted-client overlay does not apply")}finally{W.close?.()}}},{name:"per-IdP scopesAsEntitlements honors token scope even when the global flag is off",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,"run_oaa_sync"),G=M9(K,[{issuer:Y,jwksUri:`${K}/workload/certs`,audience:Y,scopesAsEntitlements:!0}]);try{await G.ready($);let W=await _0(s0(Z),G,"run_oaa_sync",void 0,"workload");z(W.allow,"the matched IdP scopesAsEntitlements honors the token scope"),z(W.decision?.metadata?.reason==="scope","allow reason is scope")}finally{G.close?.()}}},{name:"multi-IdP incoming enforces audience (cross-app token reuse is rejected at validation)",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,"run_oaa_sync"),G=M9(K,[{issuer:Y,jwksUri:`${K}/workload/certs`,audience:"https://other-app.example/api"}],{scopesAsEntitlements:!0});try{await G.ready($);let W=await _0(s0(Z),G,"run_oaa_sync",void 0,"workload");z(!W.allow,"a token whose audience does not match the trusted IdP is rejected"),z(W.reason==="unauthenticated","audience mismatch fails validation -> unauthenticated")}finally{G.close?.()}}},{name:"trustedProvisioningClients without an audience is rejected at config apply (§1.4)",fn:async()=>{let K=Q().baseUrl,Y={issuer:`${K}/workload`,audience:[],clientId:"eci-provisioning",permissions:["ion:iam:read"]},Z=_(h({id:"ecv-1-4-empty-audience",workload:{jwksUri:`${K}/workload/certs`,audience:`${K}/workload`},authz:{trustedProvisioningClients:[Y]}}),{validators:R()});try{await B5(()=>Z.ready($),"audience","an audience-less trusted provisioning client must fail config apply")}finally{Z.close?.()}}},{name:"incoming workload trust without an audience is rejected at config apply (§1.5)",fn:async()=>{let K=Q().baseUrl,Y=_(h({id:"ecv-1-5-flat-no-audience",workload:{jwksUri:`${K}/workload/certs`}}),{validators:R()});try{await B5(()=>Y.ready($),"audience","a server-only workload trust without an audience must fail config apply")}finally{Y.close?.()}let Z=_(h({id:"ecv-1-5-incoming-no-audience",workload:{incoming:[{issuer:`${K}/workload`,jwksUri:`${K}/workload/certs`}]}}),{validators:R()});try{await B5(()=>Z.ready($),"audience","an audience-less TrustedIdp incoming entry must fail config apply")}finally{Z.close?.()}}},{name:"flat server-only workload trust enforces its configured audience at runtime (§1.5)",fn:async()=>{let K=Q().baseUrl,Y=await S(K,"run_oaa_sync"),Z=_(h({id:"ecv-1-5-flat-wrong-audience",workload:{jwksUri:`${K}/workload/certs`,audience:"https://other-app.example/api"},authz:{scopesAsEntitlements:!0}}),{validators:R()});try{await Z.ready($);let G=await _0(s0(Y),Z,"run_oaa_sync",void 0,"workload");z(!G.allow,"a token minted for another audience must not authenticate"),z(G.reason==="unauthenticated","flat audience mismatch -> unauthenticated")}finally{Z.close?.()}}},{name:"subject-disabled marker vetoes the scope overlay; re-enable restores access",fn:async()=>{let K=Q().baseUrl,Y=await S(K,"run_oaa_sync"),Z=new U,G=_(h({id:"ecv-subject-disabled-overlay",workload:{jwksUri:`${K}/workload/certs`,audience:`${K}/workload`},authz:{scopesAsEntitlements:!0}}),{validators:R(),stores:{accessIndex:Z}});try{await G.ready($);let W=s0(Y),H=await _0(W,G,"run_oaa_sync",void 0,"workload");z(H.allow,"the scope overlay allows before the subject is disabled"),await Z.setSubjectDisabled("local-workload-client","workload",!0);let J=await _0(W,G,"run_oaa_sync",void 0,"workload");z(!J.allow,"a disabled subject is denied even when a scope overlay matches (deny wins)"),await Z.setSubjectDisabled("local-workload-client","workload",!1);let j=await _0(W,G,"run_oaa_sync",void 0,"workload");z(j.allow,"re-enabling the subject restores overlay-based access")}finally{G.close?.()}}},{name:"SCIM active flip sets/clears the AccessIndex subject-disabled marker (deactivate, re-activate, delete)",fn:async()=>{let K=Q().baseUrl,Y=await S(K,`${x9.id} ${H1.id}`),Z=new U,G=_(e0(K,"ecv-scim-subject-disabled"),{validators:R(),stores:{accessIndex:Z}});try{await G.ready($);let W={Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},H=await G.handler(new Request("https://ecv.test/api/ionite/iam/Users",{method:"POST",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],userName:"disable-flow@example.test",active:!0})}));z(H.status===201,`SCIM user create should succeed, got ${H.status}`);let J=await H.json();z(!await Z.isSubjectDisabled(J.id,"user"),"an active user starts enabled");let j=await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"PATCH",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"active",value:!1}]})}));z(j.status===200,`SCIM deactivate should succeed, got ${j.status}`),z(await Z.isSubjectDisabled(J.id,"user"),"PATCH active:false sets the disabled marker");let D=await Z.hasPermission(J.id,"read",void 0,"user");z(!D.allow,"a disabled subject is denied by the index"),z(D.metadata?.reason==="subject_disabled","the index denial carries reason subject_disabled");let P=await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"PUT",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],id:J.id,userName:"disable-flow@example.test",active:!0})}));z(P.status===200,`SCIM reactivate should succeed, got ${P.status}`),z(!await Z.isSubjectDisabled(J.id,"user"),"PUT active:true clears the disabled marker"),await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"PATCH",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"active",value:!1}]})})),z(await Z.isSubjectDisabled(J.id,"user"),"the marker is set again before delete");let B=await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"DELETE",headers:W}));z(B.status===204,`SCIM user delete should succeed, got ${B.status}`),z(!await Z.isSubjectDisabled(J.id,"user"),"user delete clears the disabled marker")}finally{G.close?.()}}}]}async function j8(X={}){let $=performance.now(),Q=[...J2().tests];if(X.ecv)Q.push(...U9(X)),Q.push(...j2(X));let K=[];for(let W of Q)K.push(await N(W.name,async()=>{await W.fn();return}));let Y=performance.now()-$,Z=K.filter((W)=>!W.passed).length,G=K.filter((W)=>W.details?.skipped).length;return{suite:"Authz",passed:Z===0,tests:K,duration:Y,summary:{total:K.length,passed:K.length-Z-G,failed:Z,skipped:G}}}import{ION_CAPABILITY_CATALOG as W7,ION_CAPABILITY_CATALOG_VERSION as H7}from"@ionite/server";import{silentLogger as w8}from"@ionite/core";import{ciam as D8}from"@ionite/core/server";import{InMemoryMagicLinkStore as B8,InMemorySessionStore as P8}from"@ionite/server";import{createValidators as q8}from"@ionite/valibot";var b9={customerPath:"/api/ionite/auth/customer",magicLinkPath:"/api/ionite/magic-link",magicLinkLoginPath:"/api/ionite/magic-link/login",logoutPath:"/api/ionite/auth/customer/logout",backChannelLogoutPath:"/api/ionite/auth/customer/logout/backchannel",customerProvidersPath:"/api/ionite/auth/customer/providers",customerLoginPath:"/api/ionite/auth/customer/login"},F8=["clientId","clientSecret","authority","privateKey","keyId","teamId"];function L8(X){return X.endsWith(".access")||X.endsWith(".id")||X.endsWith(".control")}function V8(X){for(let $ of X.keys()){if($.endsWith(".session"))return $.slice(0,-8);let Q=$.indexOf(".cstate");if(Q>0)return $.slice(0,Q)}return}async function y9(X,$){return N("CIAM Customer Endpoint (Unauthenticated)",async()=>{let Q=X.customerPath??"/api/ionite/auth/customer",K=await $(Q),Y=K.status!==401?await K.clone().text():"";return z(K.status===401,`Expected 401 Unauthorized, got ${K.status} (content-type=${K.headers.get("content-type")}, body=${Y.slice(0,200)})`),{details:{status:K.status}}})}async function E9(X,$){return N("CIAM Magic Link Requires Workload Auth",async()=>{let Q=X.magicLinkPath??"/api/ionite/magic-link",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userName:"jdoe",name:"Jane Doe",email:"jane@example.com"})});return z(K.status===401||K.status===404,`Expected 401 Unauthorized (or 404 if disabled), got ${K.status}`),{details:{status:K.status,endpointExists:K.status!==404}}})}async function h9(X,$){z($.endsWith("/logout"),`CIAM logoutPath must end with /logout, got: ${$}`);let Q={headers:{Accept:"application/json"}},K=await X($,Q);if(K.status!==200&&K.status!==302)K=await X($,Q);if(K.status!==200&&K.status!==302){let Y=await K.text().catch(()=>"");throw Error(`Expected 302 or 200, got ${K.status} for ${$} (content-type=${K.headers.get("content-type")??"none"}): ${Y.slice(0,500)}`)}return K}async function C9(X,$){return N("CIAM Customer Logout Endpoint",async()=>{let Q=X.logoutPath??"/api/ionite/auth/customer/logout",K=await h9($,Q),Y=K.headers.getSetCookie?.()??[],Z=Y.some((H)=>H.includes("Max-Age=0")||H.includes("expires=Thu, 01 Jan 1970")),G=Y.some((H)=>H.startsWith("ion.sso")&&(H.includes("Max-Age=0")||H.includes("expires=")));z(!G,"CIAM logout must not clear SSO (ion.sso.*) cookies");let W=[...y0(K.headers).entries()].filter(([,H])=>H===""||H.includes("Max-Age=0")).map(([H])=>H);return z(Z||W.length>0||Y.length===0,"CIAM logout should clear customer session cookies"),{details:{status:K.status,clearedCookies:W}}})}async function A9(X,$){return N("CIAM Back-Channel Logout Removed",async()=>{let Q=X.backChannelLogoutPath??"/api/ionite/auth/customer/logout/backchannel",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:""});return z(K.status===404,`Expected 404 (route removed), got ${K.status}`),{details:{status:K.status,endpointRemoved:!0}}})}async function S9(X,$){return N("CIAM Provider Discovery (Sanitized + Ordered)",async()=>{let Q=X.customerProvidersPath??"/api/ionite/auth/customer/providers",K=await $(Q);if(K.status===404)return{details:{status:404,configured:!1,skipped:!0}};z(K.status===200,`Expected 200, got ${K.status}`);let Y=await K.json();z(Array.isArray(Y.providers),"Discovery response must contain a `providers` array");let Z=Y.providers,G=[];for(let H of Z){z(typeof H.name==="string","Each provider must have a string `name`"),z(typeof H.loginUrl==="string","Each provider must include a derived `loginUrl`"),z(typeof H.displayName==="string","Each provider must include a `displayName`");for(let J of F8)z(!(J in H),`Discovery must not expose secret field "${J}"`);G.push(H.name)}let W=await $(Q);if(W.status===200){let J=((await W.json()).providers??[]).map((j)=>j.name);z(JSON.stringify(G)===JSON.stringify(J),"Provider discovery order must be stable (recommended order preserved)")}return{details:{status:200,configured:!0,count:Z.length,order:G}}})}async function k9(X,$){return N("CIAM Third-Party Login Redirect",async()=>{let Q=X.customerLoginPath??"/api/ionite/auth/customer/login",K=await $(`${Q}?provider=__ecv_unknown__`,{redirect:"manual"});z(K.status===404||K.status===400||K.status===500,`Unknown provider should be rejected (4xx/5xx), got ${K.status}`);let Y=X.customerProvidersPath??"/api/ionite/auth/customer/providers",Z=await $(Y);if(Z.status!==200)return{details:{unknownStatus:K.status,configured:!1,skipped:!0}};let W=((await Z.json()).providers??[]).find((P)=>P.type&&P.type!=="magic-link");if(!W?.name)return{details:{unknownStatus:K.status,configured:!1,skipped:!0}};let H=await $(`${Q}?provider=${encodeURIComponent(W.name)}`,{redirect:"manual"});z(H.status===302,`Expected 302 redirect to IdP, got ${H.status}`);let J=H.headers.get("location");z(Boolean(J),"Login redirect must include a Location header"),z((J??"").includes("state=")&&(J??"").includes("client_id="),"Authorization redirect must include OAuth2 state and client_id parameters");let D=(H.headers.getSetCookie?.()??[]).find((P)=>P.includes(".cstate_"));return z(Boolean(D),"Login must set a CIAM state cookie (ion.ciam.cstate_<state>)"),z(/SameSite=Lax/i.test(D),`CIAM state cookie must be SameSite=Lax for the cross-site provider callback, got: ${D}`),{details:{status:302,provider:W.name,unknownStatus:K.status,cstateCookie:D}}})}async function I9(X,$){return N("CIAM Logout Opaque-Only (no token cookies)",async()=>{let Q=X.logoutPath??"/api/ionite/auth/customer/logout",Y=(await h9($,Q)).headers.getSetCookie?.()??[],Z=Y.find((G)=>L8(G.split("=")[0]?.trim()??""));return z(!Z,`CIAM logout must not touch token cookies (opaque-only), found: ${Z}`),{details:{setCookieCount:Y.length}}})}async function m9(X,$){return N("CIAM Legacy Token Cookies Rejected",async()=>{let Q=X.logoutPath??"/api/ionite/auth/customer/logout",K=X.customerPath??"/api/ionite/auth/customer",Y=await $(Q,{headers:{Accept:"application/json"}}),Z=V8(y0(Y.headers));if(!Z)return{details:{skipped:!0,reason:"Could not derive CIAM cookie prefix from logout response"}};let G=new Map([[`${Z}.access`,"legacy-access"],[`${Z}.id`,"legacy-id"],[`${Z}.control`,btoa(JSON.stringify({token_type:"Bearer",expires_in:3600,expires:new Date(Date.now()+3600000).toISOString()}))]]),W=await $(K,{headers:{Cookie:f0(G)}});return z(W.status===401,`Legacy CIAM token cookies must not authenticate (expected 401), got ${W.status}`),{details:{status:W.status,prefix:Z}}})}async function N8(X){let $=performance.now(),Q={...b9,...X},K=r(Q),Y=[];Y.push(await y9(Q,K)),Y.push(await E9(Q,K)),Y.push(await C9(Q,K)),Y.push(await I9(Q,K)),Y.push(await m9(Q,K)),Y.push(await A9(Q,K)),Y.push(await S9(Q,K)),Y.push(await k9(Q,K));let Z=performance.now()-$,G=Y.filter((J)=>J.passed).length,W=Y.filter((J)=>!J.passed).length,H=Y.filter((J)=>J.details?.skipped).length;return{suite:"CIAM",passed:W===0,tests:Y,duration:Z,summary:{total:Y.length,passed:G-H,failed:W,skipped:H}}}async function O8(){return N("CIAM magic-link session stores no token material (§1.10)",async()=>{let X=new B8,$=new P8,Q=q8(),K=D8(Q.ciam,w8,void 0,{magicLinkUrl:"/magic-link",magicLinkLoginUrl:"/magic-link/login",sessionStore:$,magicLinkStore:X,cookiesSecure:!1});z(!!K,"ciam() must return a handle when configured");let Y={id:"cust-1",userName:"cust1",email:"c1@example.test",name:"Customer One"};await X.create("tok-1",Y,new Date(Date.now()+60000));let Z=await K?.handler(new Request("https://app.ecv.local/magic-link/login?token=tok-1"));z(!!Z,"magic-link login must produce a response");let W=(Z?.headers.getSetCookie?.()??[]).find((j)=>(j.split("=")[0]?.trim()??"").endsWith(".session"));z(!!W,"magic-link login must set an opaque session cookie");let H=W.split("=")[1]?.split(";")[0]??"";z(H.length>0,"session cookie must carry an opaque sid");let J=await $.get(H);return z(J!=null,"the magic-link session must be persisted"),z(!("tokens"in J),"a CIAM session must NOT store any signed token material (§1.10)"),z(!!J?.claims,"the opaque session still carries identity claims"),{details:{sid:H,hasTokens:J!=null&&"tokens"in J}}})}function w2(X){let $={...b9,...X},Q=r($),K={createCustomerTests:()=>[{name:"customer endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await y9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"legacy CIAM token cookies do not authenticate (opaque-only)",fn:async()=>{let Z=await m9($,Q);if(!Z.passed)throw Error(Z.error)}}],createMagicLinkTests:()=>[{name:"magic-link generation requires workload auth",fn:async()=>{let Z=await E9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"magic-link session stores no token material (§1.10)",fn:async()=>{let Z=await O8();if(!Z.passed)throw Error(Z.error)}}],createOidcLoginTests:()=>[{name:"provider discovery returns sanitized list in stable order",fn:async()=>{let Z=await S9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"third-party login redirects to IdP with state",fn:async()=>{let Z=await k9($,Q);if(!Z.passed)throw Error(Z.error)}}],createLogoutTests:()=>[{name:"customer logout endpoint clears CIAM cookies (not SSO)",fn:async()=>{let Z=await C9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"customer logout is opaque-only (no token cookies)",fn:async()=>{let Z=await I9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"customer back-channel logout endpoint is removed (404)",fn:async()=>{let Z=await A9($,Q);if(!Z.passed)throw Error(Z.error)}}]};return{tests:[...K.createCustomerTests(),...K.createMagicLinkTests(),...K.createOidcLoginTests(),...K.createLogoutTests()],ext:K}}import{generateLfvEd25519KeyPair as R8,stringifyLfvJwks as v8}from"@ionite/core/server";import{ionite as s,lfvVault as U8,secretConfigSource as f9,vaultConfig as T1}from"@ionite/server";import{createValidators as e}from"@ionite/valibot";import{generateLfvEd25519KeyPair as g,signLfvPayload as V5,stringifyLfvJwks as d}from"@ionite/core/server";import{ionite as M8,lfvVault as n}from"@ionite/server";import{createValidators as T8}from"@ionite/valibot";var V={lfvServerUrl:"http://localhost:3592/api/lfv",clientId:"ecv-lfv-client",path:"/ecv/lfv/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"};async function u(X,$,Q,K,Y){let Z=String($.request_id),G="issued_at"in $?$:{...$,issued_at:Date.now()},W=JSON.stringify(G),H=Y??await V5({privateKeyJwk:Q,requestId:Z,rawBody:W});return new Request(`http://localhost${X}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":Z,"X-LFV-Key-Id":K,"X-LFV-Signature":H},body:W})}async function j1(X,$,Q){let K=String(X.request_id),Y=JSON.stringify(X),Z=await V5({privateKeyJwk:$,requestId:K,rawBody:Y});return new Response(Y,{status:200,headers:{"Content-Type":"application/json","X-LFV-Request-Id":K,"X-LFV-Key-Id":Q,"X-LFV-Signature":Z}})}function x8(X){return{async load(){return X},async subscribe(){return},secret:{}}}function D2(){return{tests:[{name:"correctly signed OTP delivery callback is accepted",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-delivery-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),G={type:"otp",request_id:crypto.randomUUID(),path:V.path,action:"read_secret",otp:"signed-otp"},W=await Y.handleDelivery(await u(V.deliveryEndpoint,G,$,"ecv-delivery-kid"));w(W.status,503,"a verified OTP with no owner must fail loudly as unroutable"),w((await W.json()).error,"callback_unroutable","unroutable deliveries use the callback_unroutable code"),Y.close?.()}},{name:"unroutable OTP delivery undoes its replay claim so the same signed callback can be retried",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-unroutable-retry-kid"),K=n({...V,verifyJwks:d(Q)}),Y={type:"otp",request_id:crypto.randomUUID(),path:V.path,action:"read_secret",otp:"unroutable-retry",issued_at:Date.now()},Z=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-unroutable-retry-kid"));w(Z.status,503,"the first unroutable delivery is a retryable 503");let G=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-unroutable-retry-kid"));w(G.status,503,"the same signed callback must be retried, not rejected as a replay"),w((await G.json()).error,"callback_unroutable","the retry stays unroutable until a waiter or transport can own it"),K.close?.()}},{name:"read_secret returns confidential material in the TLS action response",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-update-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),Z=globalThis.fetch;globalThis.fetch=async(G,W)=>{let H=typeof G==="string"?G:G instanceof URL?G.toString():G.url,J=JSON.parse(String(W?.body));if(H.endsWith("/otp")){let j=await u(V.deliveryEndpoint,{type:"otp",request_id:J.request_id,path:J.path,action:J.action,otp:`otp-${J.request_id}`},$,"ecv-update-kid");return w((await Y.handleDelivery(j)).status,200,"OTP callback is accepted"),Response.json({status:"pending"},{status:202})}if(H.endsWith("/secrets/request"))return j1({request_id:J.request_id,status:"completed",path:J.path,data:{data:{marker:"pull-response"},metadata:{path:J.path,version:1}}},$,"ecv-update-kid");throw Error(`Unexpected LFV request: ${H}`)};try{let G=await Y.getSecret(V.path);w(G.marker,"pull-response","the action response supplies the secret")}finally{globalThis.fetch=Z,Y.close?.()}}},{name:"unsigned read_secret action response is rejected before the value is published",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-unsigned-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:200}),Y=globalThis.fetch;globalThis.fetch=async(Z,G)=>{let W=typeof Z==="string"?Z:Z instanceof URL?Z.toString():Z.url,H=JSON.parse(String(G?.body));if(W.endsWith("/otp"))return await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:H.request_id,path:H.path,action:H.action,otp:`otp-${H.request_id}`},$,"ecv-pull-unsigned-kid")),Response.json({status:"pending"},{status:202});return Response.json({request_id:H.request_id,path:H.path,data:{data:{marker:"must-not-publish"},metadata:{path:H.path,version:1}}})};try{let Z="";try{await K.getSecret(V.path)}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("missing signature headers"),"unsigned pull response must fail closed")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"tampered signed read_secret action response is rejected",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-tampered-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:200}),Y=globalThis.fetch;globalThis.fetch=async(Z,G)=>{let W=typeof Z==="string"?Z:Z instanceof URL?Z.toString():Z.url,H=JSON.parse(String(G?.body));if(W.endsWith("/otp"))return await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:H.request_id,path:H.path,action:H.action,otp:`otp-${H.request_id}`},$,"ecv-pull-tampered-kid")),Response.json({status:"pending"},{status:202});let J=await j1({request_id:H.request_id,path:H.path,data:{data:{marker:"original"},metadata:{path:H.path,version:1}}},$,"ecv-pull-tampered-kid");return new Response(JSON.stringify({request_id:H.request_id,path:H.path,data:{data:{marker:"tampered"},metadata:{path:H.path,version:1}}}),{status:200,headers:J.headers})};try{let Z="";try{await K.getSecret(V.path)}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("signature verification failed"),"tampered pull response must fail closed")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"read_secret action response is bound to the redeem request_id",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-request-id-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:200}),Y=globalThis.fetch;globalThis.fetch=async(Z,G)=>{let W=typeof Z==="string"?Z:Z instanceof URL?Z.toString():Z.url,H=JSON.parse(String(G?.body));if(W.endsWith("/otp"))return await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:H.request_id,path:H.path,action:H.action,otp:`otp-${H.request_id}`},$,"ecv-pull-request-id-kid")),Response.json({status:"pending"},{status:202});return j1({request_id:crypto.randomUUID(),path:H.path,data:{data:{marker:"wrong-cycle"},metadata:{path:H.path,version:1}}},$,"ecv-pull-request-id-kid")};try{let Z="";try{await K.getSecret(V.path)}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("header does not match"),"pull response must match the redeem request_id")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"terminal read_secret denial does not retry the OTP cycle",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-terminal-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:500,retryInterval:10}),Y=globalThis.fetch,Z=0,G=0;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z+=1,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-pull-terminal-kid")),Response.json({status:"pending"},{status:202});return G+=1,Response.json({error:"permission_denied"},{status:403})};try{let W=!1;try{await K.getSecret(V.path)}catch{W=!0}z(W,"terminal denial must reject the read"),w(Z,1,"terminal denial must not mint another OTP"),w(G,1,"terminal denial must not retry the action")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"404 read_secret is terminal per-read and carries code not_found",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-404-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:500,retryInterval:10}),Y=globalThis.fetch,Z=0,G=0;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z+=1,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-pull-404-kid")),Response.json({status:"pending"},{status:202});return G+=1,Response.json({error:"not_found"},{status:404})};try{let W;try{await K.getSecret(V.path)}catch(H){W=H}z(W instanceof Error,"404 must reject the read"),w(W.status,404,"404 must surface as status 404"),w(W.code,"not_found","404 must classify as not_found"),w(Z,1,"404 must not mint another OTP"),w(G,1,"404 must not retry the action")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"forged delivery signature is rejected with 401 invalid_signature",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-forged-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),G={request_id:crypto.randomUUID(),path:V.path,data:{data:{marker:"forged"},metadata:{path:V.path,version:1}}},W=await Y.handleDelivery(await u(V.deliveryEndpoint,G,$,"ecv-forged-kid","AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));w(W.status,401,"forged delivery should return 401");let H=await W.json();w(H.error,"invalid_signature","error code should be invalid_signature")}},{name:"delivery without signature headers is rejected",fn:async()=>{let{publicJwks:$}=await g("ecv-missing-kid"),K=await n({...V,verifyJwks:d($)}).handleDelivery(new Request(`http://localhost${V.deliveryEndpoint}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request_id:crypto.randomUUID(),path:V.path,data:{data:{},metadata:{path:V.path,version:1}}})}));w(K.status,401,"unsigned delivery should return 401")}},{name:"delivery rejects request_id header/body mismatch",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-mismatch-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),Z=crypto.randomUUID(),G=JSON.stringify({request_id:Z,path:V.path,data:{data:{},metadata:{path:V.path,version:1}}}),W=await V5({privateKeyJwk:$,requestId:Z,rawBody:G}),H=await Y.handleDelivery(new Request(`http://localhost${V.deliveryEndpoint}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":crypto.randomUUID(),"X-LFV-Key-Id":"ecv-mismatch-kid","X-LFV-Signature":W},body:G}));w(H.status,401,"mismatched request_id should return 401")}},{name:"delivery is rejected when verifyJwks is not configured",fn:async()=>{let{privateJwk:$}=await g("ecv-no-jwks-kid"),Q=n({...V}),Y={request_id:crypto.randomUUID(),path:V.path,data:{data:{},metadata:{path:V.path,version:1}}},Z=await Q.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-no-jwks-kid"));w(Z.status,401,"delivery without verifyJwks should return 401")}},{name:"correctly signed events callback is accepted and key_id maps to the listener keyId",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-events-kid"),K=n({...V,verifyJwks:d(Q)}),Y=[],Z=K.subscribeEvents?.((D)=>{Y.push({event:D.event,...D.keyId?{keyId:D.keyId}:{}})}),W={request_id:crypto.randomUUID(),path:V.path,event:"updated"},H=await K.handleEvents(await u(V.eventsEndpoint,W,$,"ecv-events-kid"));w(H.status,200,"signed events should return 200");let J={request_id:crypto.randomUUID(),path:V.path,event:"kms_key_parked",key_id:"k-parked"},j=await K.handleEvents(await u(V.eventsEndpoint,J,$,"ecv-events-kid"));w(j.status,200,"signed issuer events should return 200"),w(JSON.stringify(Y),JSON.stringify([{event:"updated"},{event:"kms_key_parked",keyId:"k-parked"}]),"listeners receive the wire key_id as keyId (metadata-only)"),Z?.(),K.close?.()}},{name:"forged events signature is rejected with 401",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-events-forged-kid"),K=n({...V,verifyJwks:d(Q)}),Z={request_id:crypto.randomUUID(),path:V.path,event:"revoked"},G=await K.handleEvents(await u(V.eventsEndpoint,Z,$,"ecv-events-forged-kid","BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"));w(G.status,401,"forged events should return 401")}},{name:"replayed delivery callback (same signature) is rejected as a replay",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-replay-kid"),K=n({...V,verifyJwks:d(Q)},{put:async()=>{},take:async()=>{return}}),Y={type:"otp",request_id:crypto.randomUUID(),path:V.path,action:"read_secret",otp:"single-use-replay-test",issued_at:Date.now()},Z=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-replay-kid"));w(Z.status,200,"first delivery should be accepted");let G=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-replay-kid"));w(G.status,401,"an exact replay of the same signed callback must be rejected");let W=await G.json();w(W.error,"invalid_signature","replay rejection uses the invalid_signature code")}},{name:"stale delivery callback (issued_at outside freshness window) is rejected",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-stale-kid"),K=n({...V,verifyJwks:d(Q)}),Y={request_id:crypto.randomUUID(),path:V.path,issued_at:Date.now()-600000,data:{data:{marker:"stale"},metadata:{path:V.path,version:1}}},Z=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-stale-kid"));w(Z.status,401,"a stale callback must be rejected")}},{name:"delivery callbacks reject secret values and accept OTP work items only",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-otp-only-kid"),Y=await n({...V,verifyJwks:d(Q)}).handleDelivery(await u(V.deliveryEndpoint,{request_id:crypto.randomUUID(),path:V.path,data:{data:{marker:"must-not-transit"},metadata:{path:V.path,version:1}}},$,"ecv-otp-only-kid"));w(Y.status,400,"a value-bearing callback is rejected"),w((await Y.json()).error,"invalid_request","the callback is rejected as invalid_request")}},{name:"callback fan-out remains scoped by verifyJwks trust identity",fn:async()=>{let $=await g("ecv-trust-a"),Q=await g("ecv-trust-b"),K={...V,path:"/ecv/lfv/trust-shared"},Y={put:async()=>{},take:async()=>{return}},Z=n({...K,verifyJwks:d($.publicJwks)},Y),G=n({...K,verifyJwks:d(Q.publicJwks)},Y);w(Z.callbackKey,G.callbackKey,"both sources share one routing key"),z(Z.trustId!==G.trustId,"sources preserve distinct trust identities");let W=await Z.handleDelivery(await u(K.deliveryEndpoint,{type:"otp",request_id:crypto.randomUUID(),path:K.path,action:"read_secret",otp:"trust-a-only"},$.privateJwk,"ecv-trust-a"));w(W.status,200,"source A accepts its trusted OTP"),Z.close?.(),G.close?.()}},{name:"failed post-event refresh marks the source degraded (isAvailable() false), not a stale serve",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-degraded-kid"),K=d(Q),Y=n({...V,verifyJwks:K,deliveryTimeout:150,retryInterval:40});w(Y.isAvailable?.(),!0,"a newly-created source is available"),await Y.handleEvents(await u(V.eventsEndpoint,{request_id:crypto.randomUUID(),path:V.path,event:"updated"},$,"ecv-degraded-kid"));let Z=Date.now()+3000;while(Y.isAvailable?.()!==!1&&Date.now()<Z)await new Promise((G)=>setTimeout(G,25));w(Y.isAvailable?.(),!1,"a failed post-event refresh must mark the source degraded via isAvailable()"),Y.close?.()}},{name:"cacheMaxAgeSeconds bounds cache hits: a stale entry forces a fresh read",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-maxage-kid"),K=d(Q),Y=n({...V,verifyJwks:K,cacheMaxAgeSeconds:0.05,deliveryTimeout:150,retryInterval:40}),Z=globalThis.fetch,G=0;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return await Y.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-maxage-kid")),Response.json({status:"pending"},{status:202});return G+=1,j1({request_id:j.request_id,path:j.path,data:{data:{marker:G===1?"fresh":"refreshed"},metadata:{path:j.path,version:G}}},$,"ecv-maxage-kid")};try{w((await Y.getSecret(V.path)).marker,"fresh","the initial pull is cached"),await new Promise((W)=>setTimeout(W,120)),w((await Y.getSecret(V.path)).marker,"refreshed","an aged cache entry forces another pull"),w(G,2,"the action endpoint was called twice")}finally{globalThis.fetch=Z,Y.close?.()}}},{name:"raised kms_slots demand bypasses the cache and is carried on OTP issue and redeem",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-slots-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z=0,G=[],W=[];globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H instanceof URL?H.toString():H.url,D=JSON.parse(String(J?.body));if(j.endsWith("/otp"))return G.push(D.kms_slots),await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:D.request_id,path:D.path,action:D.action,otp:`otp-${D.request_id}`},$,"ecv-kms-slots-kid")),Response.json({status:"pending"},{status:202});return W.push(D.kms_slots),Z+=1,j1({request_id:D.request_id,path:D.path,data:{data:{marker:`grant-v${Z}`},metadata:{path:D.path,version:Z}}},$,"ecv-kms-slots-kid")};try{let H=[],J=await K.subscribe(V.path,(q)=>{H.push(q.metadata.version)});w(Z,1,"the initial subscribe performs one read cycle"),w(G[0],void 0,"an undeclared demand omits kms_slots from the OTP request"),w(W[0],void 0,"an undeclared demand omits kms_slots from the redeem");let j,D=await K.subscribe(V.path,(q)=>{j=q.metadata.version},{kmsSlots:3});w(Z,2,"a raised demand bypasses the cache and forces a fresh read"),w(G[1],3,"the raised demand is carried as kms_slots on OTP issuance"),w(W[1],3,"the raised demand is carried as kms_slots on the redeem"),w(j,2,"the raising subscriber receives the freshly materialized grant");let P,B=await K.subscribe(V.path,(q)=>{P=q.metadata.version},{kmsSlots:2});w(Z,2,"a demand at or below the recorded high-water mark is served from cache"),w(P,2,"the cached grant already satisfies the lower demand"),J(),D(),B()}finally{globalThis.fetch=Y,K.close?.()}}},{name:"kms_park OTP issue and redeem carry key_id, status, and reason and never the secret value",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-park-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z,G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z=j.action,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-kms-park-kid")),Response.json({status:"pending"},{status:202});return G={action:j.action,key_id:j.key_id,status:j.status,reason:j.reason,value:j.value},Response.json({request_id:j.request_id,parked:!0,disposition:"cooldown"})};try{let W=await K.parkKmsKey(V.path,{keyId:"k-parked",status:429,reason:"rate limited"});w(Z,"kms_park","OTP issuance must name the kms_park action"),w(G?.key_id,"k-parked","the redeem body carries the resolved key id"),w(G?.status,429,"the redeem body carries the reported status"),w(G?.reason,"rate limited","the redeem body carries the reported reason"),z(G?.value===void 0,"the redeem body must never include a key value"),w(W.parked,!0,"the source returns the vault park result"),w(W.disposition,"cooldown","the source returns the vault disposition")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"kms_add_keys OTP issue and redeem carry issued keys and return assigned ids",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-add-keys-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z,G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z=j.action,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-kms-add-keys-kid")),Response.json({status:"pending"},{status:202});return G=j.keys,Response.json({request_id:j.request_id,keys:[{id:"issued-1"}]})};try{let W=await K.addKmsKeys(V.path,{keys:[{value:"oem-token"}]});w(Z,"kms_add_keys","OTP issuance must name the kms_add_keys action"),w(JSON.stringify(G),JSON.stringify([{value:"oem-token"}]),"the redeem body carries issued keys"),w(W.keys[0]?.id,"issued-1","the source returns assigned key ids")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"kms_revoke_key OTP issue and redeem carry key_id and reason",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-revoke-key-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z,G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z=j.action,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-kms-revoke-key-kid")),Response.json({status:"pending"},{status:202});return G={key_id:j.key_id,reason:j.reason},Response.json({request_id:j.request_id,revoked:!0})};try{let W=await K.revokeKmsKey(V.path,{keyId:"k-revoked",reason:"compromise"});w(Z,"kms_revoke_key","OTP issuance must name the kms_revoke_key action"),w(G?.key_id,"k-revoked","the redeem body carries the key id"),w(G?.reason,"compromise","the redeem body carries the reason"),w(W.revoked,!0,"the source returns the vault revoke result")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"two LFV sources on one serverUrl|path with different verifyJwks are rejected at apply",fn:async()=>{let $=d((await g("ecv-apply-a")).publicJwks),Q=d((await g("ecv-apply-b")).publicJwks),K="/ecv/lfv/apply-shared",Y={id:"ecv-lfv-dual-trust",secrets:{SRC_A:{type:"vault",lfv:{lfvServerUrl:V.lfvServerUrl,clientId:"ecv-apply-client",path:"/ecv/lfv/apply-shared",deliveryEndpoint:"/api/a/lfv/delivery",eventsEndpoint:"/api/a/lfv/events",verifyJwks:$}},SRC_B:{type:"vault",lfv:{lfvServerUrl:V.lfvServerUrl,clientId:"ecv-apply-client",path:"/ecv/lfv/apply-shared",deliveryEndpoint:"/api/b/lfv/delivery",eventsEndpoint:"/api/b/lfv/events",verifyJwks:Q}}}},Z=M8(x8(Y),{validators:T8(),lfv:{singleProcess:!0}}),G=!1,W="";try{await Z.ready(2000)}catch(H){G=!0,W=H instanceof Error?H.message:String(H)}Z.close?.(),z(G,"a dual-trust LFV config must reject ion.ready() at apply time"),z(W.toLowerCase().includes("trust key"),`apply rejection should explain the single-trust-key-per-path invariant (got: ${W})`)}}]}}async function _8(){let X=performance.now(),{tests:$}=D2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"LFV callbacks",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}function I1(X,$={}){return Object.assign(Error(X),$)}function M1(X){if(X.length===0)throw Error("sequenceConfigSource requires at least one step");let $=0;return{attempts:()=>$,async load(){$+=1;let Q=X[Math.min($-1,X.length-1)];if(Q===void 0)throw Error("sequenceConfigSource step unexpectedly missing");if(Q instanceof Error)throw Q;if(typeof Q==="function")return await Q();return Q},async subscribe(){return},secret:{}}}async function N0(X){await new Promise(($)=>setTimeout($,X))}function E0(X){let $=X,Q;return{async load(){return $},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){$=K,Q?.(K)},secret:{}}}function p9(X,$){let Q;return{async load(){return await N0($),X},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){Q?.(K)},secret:{}}}var w0={jwksUri:"http://localhost/workload/certs",issuer:"http://localhost/workload",audience:"http://localhost/workload"};function N5(){return[{name:"module readiness re-arms when a module is de-configured and re-configured (§5.1)",fn:async()=>{let X=E0({id:"rearm",workload:w0}),$=s(X,{validators:e()});await $.ready(500),z($.workload?.isReady?.()===!0,"Expected workload ready after initial config"),X.push({id:"rearm"}),await N0(20),z($.workload?.isReady?.()===!1,"Expected workload.isReady() false after de-configuration");let Q=!1;try{await $.workload?.ready?.(50)}catch{Q=!0}z(Q,"Expected re-armed workload.ready(50) to reject while de-configured"),X.push({id:"rearm",workload:w0}),await N0(20),z($.workload?.isReady?.()===!0,"Expected workload.isReady() true after re-configuration"),await $.workload?.ready?.(200),$.close?.()}},{name:"async beforeChange override is awaited and applied (§5.2)",fn:async()=>{let X=E0({id:"bc-1"}),$=s(X,{validators:e(),beforeChange:async(Q)=>{return await N0(5),{config:{...Q,id:"bc-1-overridden"}}}});await $.ready(500),w($.config?.id,"bc-1-overridden","Expected async beforeChange override to be applied"),$.close?.()}},{name:"awaiting ion.ready() inside beforeChange on initial load fails fast (§5.2)",fn:async()=>{let X=E0({id:"deadlock-1"}),$=!1,Q=s(X,{validators:e(),beforeChange:async()=>{try{await Q.ready(100)}catch{$=!0}return}});await Q.ready(500),z($,"Expected ion.ready() inside beforeChange to reject (deadlock guard)"),z(Q.isReady(),"Expected instance to still become ready after beforeChange returns"),Q.close?.()}},{name:"no-argument ion.ready() is bounded by readyTimeoutMs; 0 opts into an indefinite wait",fn:async()=>{let X=p9({id:"ready-default-bound"},300),$=s(X,{validators:e(),readyTimeoutMs:50}),Q=!1;try{await $.ready()}catch{Q=!0}z(Q,"Expected the no-argument ion.ready() to reject after the readyTimeoutMs bound"),await $.ready(0),z($.isReady(),"Expected ion.ready(0) to wait indefinitely and resolve once config applied"),$.close?.()}},{name:"throwing afterChange on initial load rejects ready() (§5.4)",fn:async()=>{let X=E0({id:"ac-1",workload:w0}),$=s(X,{validators:e(),afterChange:()=>{throw Error("afterChange boom (initial)")}}),Q=!1;try{await $.ready(500)}catch{Q=!0}z(Q,"Expected a throwing initial afterChange to reject ready()"),$.close?.()}},{name:"setStores() re-applies config and runs afterChange (§16.2)",fn:async()=>{let X=0,$=!1,Q=E0({id:"setstores-1",workload:w0}),K=s(Q,{validators:e(),afterChange:(Z,G,W)=>{X+=1,$=typeof W.setStores==="function"}});await K.ready(500);let Y=X;z(Y>=1,"Expected afterChange to run on the initial apply"),await K.setStores?.({}),z(X>Y,"Expected afterChange to run again after setStores() re-applies config"),z($,"Expected afterChange to receive a modifiable framework config (with setStores) on the store swap"),K.close?.()}},{name:"throwing afterChange on a live push keeps last-known-good applied (§5.4)",fn:async()=>{let X=0,$=E0({id:"aclkg"}),Q=s($,{validators:e(),afterChange:()=>{if(X+=1,X>=2)throw Error("afterChange boom (live)")}});await Q.ready(500),z(Q.isReady(),"Expected ready after first apply"),$.push({id:"aclkg",workload:w0}),await N0(20),z(Q.isReady(),"Expected instance to stay ready (LKG) after a throwing live afterChange"),z(Q.config?.workload!==void 0,"Expected the pushed config to still apply despite afterChange throwing"),Q.close?.()}},{name:"a live update that changes the immutable RemoteConfig.id is rejected and keeps last-known-good (§2.4)",fn:async()=>{let X=E0({id:"immutable-id",workload:w0}),$=s(X,{validators:e()});await $.ready(500),w($.config?.id,"immutable-id","Expected the initial id to apply"),X.push({id:"different-id",workload:w0}),await N0(20),z($.isReady(),"Expected the instance to stay ready after a rejected id change"),w($.config?.id,"immutable-id","Expected the immutable id change to be rejected (LKG retained)"),z($.configStatus?.lastError!==void 0,"Expected configStatus.lastError to record the rejected immutable-id change"),$.close?.()}},{name:"setStores() and a concurrent live push are serialized; the push is reflected (§5.1)",fn:async()=>{let X=E0({id:"s51",workload:w0}),$=s(X,{validators:e(),beforeChange:async(K)=>{return await N0(50),{config:K}}});await $.ready(1000),z($.config?.capabilitiez!==!0,"Expected capabilitiez unset before the push");let Q=$.setStores?.({});X.push({id:"s51",workload:w0,capabilitiez:!0}),await Q,await N0(150),w($.config?.capabilitiez,!0,"Expected the concurrent live push to be reflected after setStores (serialized, not reverted)"),$.close?.()}},{name:"a wait-mode push that lands during a slow initial load wins over the stale load (§5.2)",fn:async()=>{let X=p9({id:"race",workload:w0},80),$=s(X,{validators:e()});X.push({id:"race",workload:w0,capabilitiez:!0}),await $.ready(500),w($.config?.capabilitiez,!0,"Expected the wait-mode push (v2) to be the applied config"),await N0(120),w($.config?.capabilitiez,!0,"Expected the stale initial load to be skipped (push retained)"),$.close?.()}},{name:"a secrets source config unchanged across an sso-only re-apply keeps a stable, ready source (§7.9)",fn:async()=>{let X=E0({id:"memo",workload:w0,secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),$=s(X,{validators:e()});await $.ready(500),z($.secrets?.isReady?.()===!0,"Expected the secrets module ready after initial apply");let Q=$.secrets?.getSecretsSource("primary");X.push({id:"memo",workload:w0,sso:{clientId:"c",clientSecret:"s",authority:"http://localhost/idp"},secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),await N0(30),z($.secrets?.isReady?.()===!0,"Expected the secrets module to stay ready after an sso-only change");let K=$.secrets?.getSecretsSource("primary");z(Q===K,"Expected the unchanged secrets source handle to be reused (memoized) across apply"),$.close?.()}}]}function B2(X){return{type:"vault",getFullSecret:async()=>({data:X,metadata:{version:1}})}}function g9(X){return{type:"vault",getSecret:async()=>X}}async function m1(X,$){try{await X()}catch(Q){if($&&!$(Q))throw Error(`Threw an unexpected error: ${Q instanceof Error?Q.message:String(Q)}`);return Q}throw Error("Expected the operation to throw, but it resolved")}function O5(){return[{name:"RemoteConfig credential wins over a framework-set secrets credential (§2.1)",fn:async()=>{let X=e(),$={},Q={...X,secrets:{...X.secrets,validateSourceConfig:(Z,G)=>{$[Z]=G}}},K=E0({id:"merge-1",secrets:{primary:{type:"vault",url:"http://localhost/vault",token:"REMOTE-TOKEN"}}}),Y=s(K,{validators:Q,secrets:{primary:{token:"CODE-TOKEN"}}});await Y.ready(500),w($.primary?.token,"REMOTE-TOKEN","Expected the merged secrets config to use the RemoteConfig token, not the framework token"),Y.close?.()}},{name:"unimplemented secrets source types (aws/gcp/dev) fail at config apply, not first access (§4.3)",fn:async()=>{for(let X of["aws","gcp","dev"]){let $=E0({id:`unimpl-${X}`,secrets:{x:{type:X}}}),Q=s($,{validators:e()}),K=!1;try{await Q.ready(300)}catch{K=!0}z(K,`Expected secrets source type "${X}" to fail readiness at config apply`),Q.close?.()}}},{name:"vault config-source rejects an empty payload as recoverable and a wrong-shape payload as invalid (§2.2)",fn:async()=>{let X=T1({vault:B2({}),path:"ion/config"});await m1(()=>X.load(),(Y)=>Y instanceof Error&&(Y.name==="VaultConfigWaitingForSecretError"||Y.code==="not_found"||/empty|missing/i.test(Y.message)));let $=T1({vault:B2({foo:1}),path:"ion/config"});await m1(()=>$.load(),(Y)=>Y instanceof Error&&/invalid payload|not a remoteconfig/i.test(Y.message));let K=await T1({vault:B2({data:{id:"cfg-ok"}}),path:"ion/config"}).load();w(K.id,"cfg-ok","Expected a valid wrapped RemoteConfig payload to be unwrapped")}},{name:"vault config-source enforces the ten-minute HTTP polling floor",fn:async()=>{let X=B2({data:{id:"polling-ttl"}});await m1(async()=>T1({vault:X,path:"ion/config",ttl:599999}),(K)=>K instanceof Error&&/10 minutes|600000/.test(K.message));let Q=await T1({vault:X,path:"ion/config",ttl:600000}).load();w(Q.id,"polling-ttl","the minimum safe HTTP polling TTL is accepted")}},{name:"secret config-source rejects an invalid ConfigLocator payload (§2.2)",fn:async()=>{let X=f9(g9({type:"not-a-real-type"}),"boot/locator");await m1(()=>X.load(),(Q)=>Q instanceof Error&&/invalid payload|unknown type/i.test(Q.message));let $=f9(g9("vault"),"boot/locator");await m1(()=>$.load(),(Q)=>Q instanceof Error&&/invalid payload|expected an object/i.test(Q.message))}},{name:"a configured beforeChange re-applies even when the effective config is unchanged (§2.3)",fn:async()=>{let X=0,$=E0({id:"normalize-me",workload:w0}),Q=s($,{validators:e(),beforeChange:(Y)=>({config:{...Y,id:"normalized"}}),afterChange:()=>{X+=1}});await Q.ready(500);let K=X;z(K>=1,"Expected at least one apply on the initial load"),$.push({id:"normalize-me-2",workload:w0}),await N0(30),z(X>K,"Expected beforeChange-present pipeline to re-apply on a normalized push"),Q.close?.()}},{name:"secrets module stays not-ready while a declared source lacks a ConfigSource type (§4.5)",fn:async()=>{let X=E0({id:"sec-ready",workload:w0}),$=s(X,{validators:e(),secrets:{needsRemote:{}}});await $.ready(500),z($.isReady(),"Expected the instance to be ready (config applied) on initial load"),z($.secrets?.isReady?.()===!1,"Expected the secrets module to be not-ready while a declared source has no ConfigSource type"),X.push({id:"sec-ready",workload:w0,secrets:{needsRemote:{type:"vault",url:"http://localhost/vault"}}}),await N0(30),z($.secrets?.isReady?.()===!0,"Expected the secrets module to become ready once the ConfigSource supplies the source type"),$.close?.()}}]}function M5(){return[{name:"retries recoverable missing config and then becomes ready",fn:async()=>{let X=M1([I1("Waiting for vault config secret at ion/test",{code:"not_found",status:404,sourceType:"vault"}),{id:"retry-ok"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await $.ready(250),w(X.attempts(),2,"Expected one retry after recoverable missing config"),$.close?.()}},{name:"retries an LFV 404 not_found then becomes ready (§2.1 transport parity)",fn:async()=>{let{privateJwk:X,publicJwks:$}=await R8("ecv-lfv-404-retry-kid"),Q=U8({...V,verifyJwks:v8($),deliveryTimeout:500,retryInterval:10}),K=T1({path:V.path,vault:Q}),Y=0,Z={attempts:()=>Y,load:async()=>{return Y+=1,K.load()},subscribe:async()=>{return},secret:K.secret},G=globalThis.fetch;globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H instanceof URL?H.toString():H.url,D=JSON.parse(String(J?.body));if(j.endsWith("/otp")){if(Y<=1)return Response.json({error:"not_found"},{status:404});return await Q.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:D.request_id,path:D.path,action:D.action,otp:`otp-${D.request_id}`},X,"ecv-lfv-404-retry-kid")),Response.json({status:"pending"},{status:202})}if(j.endsWith("/secrets/request"))return j1({request_id:D.request_id,status:"completed",path:D.path,data:{data:{id:"lfv-404-retry-ok"},metadata:{path:D.path,version:1}}},X,"ecv-lfv-404-retry-kid");throw Error(`Unexpected LFV request: ${j}`)};let W=s(Z,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});try{await W.ready(250),z(W.isReady(),"Expected LFV 404 not_found to retry and then become ready"),w(Z.attempts(),2,"Expected one retry after an LFV 404"),w(W.config?.id,"lfv-404-retry-ok","Expected the recovered LFV config to apply")}finally{globalThis.fetch=G,W.close?.(),Q.close?.()}}},{name:"fails fast for unrecoverable auth errors",fn:async()=>{let X=M1([I1("Vault returned 403 Forbidden (auth error)",{status:403,sourceType:"vault"}),{id:"should-not-load"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}}),Q=!1;try{await $.ready(250)}catch{Q=!0}await N0(10),z(Q,"Expected auth failure to reject readiness"),w(X.attempts(),1,"Expected auth failure not to retry"),$.close?.()}},{name:"retries unclassified (unknown) errors instead of failing fast (§2.1)",fn:async()=>{let X=M1([Error("unexpected config loader bug"),{id:"unknown-retry-ok"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await $.ready(250),z($.isReady(),"Expected an unknown error to retry and then become ready"),w(X.attempts(),2,"Expected one retry after an unknown (retry-by-default) error"),$.close?.()}},{name:"retries transient transport codes (EAI_AGAIN) then becomes ready (§2.1)",fn:async()=>{let X=M1([I1("getaddrinfo EAI_AGAIN vault.internal",{code:"EAI_AGAIN",sourceType:"vault"}),I1("getaddrinfo EAI_AGAIN vault.internal",{code:"EAI_AGAIN",sourceType:"vault"}),{id:"eai-ok"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await $.ready(500),z($.isReady(),"Expected EAI_AGAIN to retry and then become ready"),w(X.attempts(),3,"Expected two retries after EAI_AGAIN before success"),$.close?.()}},{name:"fails fast for a structured 4xx (403) load error (§2.1)",fn:async()=>{let X=M1([I1("Vault returned 403 Forbidden",{status:403,sourceType:"vault"}),{id:"should-not-load"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}}),Q=!1;try{await $.ready(250)}catch{Q=!0}await N0(10),z(Q,"Expected a 403 load to fail fast"),w(X.attempts(),1,"Expected a provably-unrecoverable 403 not to retry"),$.close?.()}},{name:"handler returns 503 immediately while the instance is not ready",fn:async()=>{let X=M1([async()=>{return await N0(100),{id:"slow-config"}}]),$=s(X,{validators:e()});z(!$.isReady(),"Expected instance to start not ready");let Q=await $.handler(new Request("http://localhost/api/ionite/auth/user"));w(Q.status,503,"Expected handler to return 503 immediately when not ready"),await $.ready(250),z($.isReady(),"Expected instance to be ready after config applied"),$.close?.()}}]}import{ION_CAPABILITIEZ_READ as b8,ION_IAM_GROUPS_WRITE as y8,ION_IAM_READ as E8,ION_IAM_USERS_WRITE as h8,ION_TENANT_MANAGE as C8}from"@ionite/server";var T5=`${E8.id} ${h8.id} ${y8.id}`,x5=b8.id,f1=C8.id;async function k0(X,$,Q="local-workload-client",K="local-workload-secret"){let Y=`${X.replace(/\/$/,"")}/workload/token`,Z=await fetch(Y,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:Q,client_secret:K,...$?{scope:$}:{}}).toString()});if(!Z.ok){let W=await Z.text();throw Error(`Failed to get tenant auth token: ${Z.status} ${W}`)}let G=await Z.json();if(!G.access_token)throw Error("Tenant auth token response missing access_token");return G.access_token}function _5(){return[{name:"readyPath defaults to basePath + /readyz (/api/ionite/readyz) (§17.3)",fn:async()=>{w(L1({}),"/api/ionite/readyz","Expected the default readyPath to derive from /api/ionite"),w(L1({basePath:"/api/ionite/"}),"/api/ionite/readyz","Expected a trailing-slash basePath to normalize")}},{name:"readyPath derives from a custom basePath (§17.3)",fn:async()=>{w(L1({basePath:"/custom/mount"}),"/custom/mount/readyz","Expected readyPath to derive from the provided basePath")}},{name:"an explicit readyPath always wins over basePath (§17.3)",fn:async()=>{w(L1({readyPath:"/api/readyz",basePath:"/api/ionite"}),"/api/readyz","Expected an explicit readyPath to override basePath derivation")}}]}import{ION_SCIM_USER_EXTENSION_URN as R0}from"@ionite/core";import{createValidators as A8}from"@ionite/valibot";var R5=A8(),S8="urn:ionite:params:scim:schemas:extension:authz:2.0:Group",P2="urn:ietf:params:scim:schemas:core:2.0:User",p1="urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",k8="urn:ietf:params:scim:api:messages:2.0:ListResponse",u9="ecid:ecv.ionite.test:01J9F3Z8QK7M2N4P6R8T0V2W4X";function m(X,$){return z(typeof X==="object"&&X!==null&&!Array.isArray(X),`${$} must be an object`),X}function v0(X,$){return z(typeof X==="string",`${$} must be a string`),X}function l9(X,$){return z(typeof X==="number",`${$} must be a number`),X}function g1(X,$){return z(Array.isArray(X)&&X.every((Q)=>typeof Q==="string"),`${$} must be a string array`),X}function $1(X,$){return z(Array.isArray(X),`${$} must be an array`),X}function q2(X,$){let Q=X[S8];if(Q===void 0)return;let K=m(Q,`${$} authz extension`);if(K.type!==void 0)z(K.type==="group"||K.type==="role"||K.type==="permission",`${$} authz extension type must be group, role, or permission`);if(K.requestable!==void 0)z(typeof K.requestable==="boolean",`${$} authz extension requestable must be a boolean`);if(K.requestUrl!==void 0)z(typeof K.requestUrl==="string",`${$} authz extension requestUrl must be a string`),new URL(K.requestUrl);if(K.description!==void 0)z(typeof K.description==="string",`${$} authz extension description must be a string`)}function v5(X,$,Q,K){let Y=m($.meta,`Created ${Q} meta`),Z=v0(Y.location,`Created ${Q} meta.location`),G=X.headers.get("Location");z(G===Z,`Location header must equal meta.location for created ${Q}`),z(new URL(Z).pathname.endsWith(`/${Q}/${encodeURIComponent(K)}`),`Created ${Q} location must address the new resource id`)}var d9={scimPath:"/api/iam",testUser:{userName:"ecv-test-user@example.com",displayName:"ECV Test User",emails:[{value:"ecv-test-user@example.com",primary:!0}],name:{givenName:"ECV Test",familyName:"User"},[p1]:{department:"Platform",costCenter:"ENG-001",organization:"ionite"},[R0]:{ecid:u9,accountClass:"daily"}},testGroup:{displayName:"ECV Test Group",externalId:"ecv-test-group-001"}};async function H0(X){let $=typeof X.ion==="function"?X.ion():X.ion;if($!==null&&$!==void 0){if(await $.ready(X.timeout??15000),!$.workload)throw Error("Workload management is required for IAM tests. Ionite instance provided but workload is not configured.");let K=$.workload;return`Bearer ${typeof K.getServerToken==="function"?await K.getServerToken():await K.getToken()}`}return`Bearer ${await k0(V1(X.ecvUrl,"IAM validation ecvUrl when ion is not provided"),T5)}`}async function I8(X){return`Bearer ${await k0(V1(X.ecvUrl,"IAM validation ecvUrl when ion is not provided"))}`}async function c9(X,$){return N("IAM Users Endpoint Schema",async()=>{let Q=await H0(X),K=await $(`${X.scimPath}/Users`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(K.status===200,`Expected 200 OK, got ${K.status}`);let Y=m(await K.json(),"SCIM users response"),Z=g1(Y.schemas,"Response schemas"),G=l9(Y.totalResults,"Response totalResults"),W=$1(Y.Resources,"Response Resources");return z(Z.includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"),"Response missing SCIM ListResponse schema"),{details:{totalResults:G,resourceCount:W.length}}})}async function m8(X,$){return N("IAM Groups Endpoint Schema",async()=>{let Q=await H0(X),K=await $(`${X.scimPath}/Groups`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(K.status===200,`Expected 200 OK, got ${K.status}`);let Y=m(await K.json(),"SCIM groups response"),Z=g1(Y.schemas,"Response schemas"),G=l9(Y.totalResults,"Response totalResults"),W=Y.Resources===void 0?[]:$1(Y.Resources,"Response Resources");for(let H of W)q2(m(H,"SCIM group resource"),"SCIM group resource");return z(Z.includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"),"Response missing SCIM ListResponse schema"),{details:{totalResults:G,resourceCount:W.length}}})}async function n9(X,$){if(!X.testUser)return{...W1("IAM Create User","No test user configured"),userId:void 0};let Q=X.testUser,K=await N("IAM Create User",async()=>{let Y=await H0(X),Z={schemas:[P2,p1,...Q[R0]?[R0]:[]],...Q},G=await $(`${X.scimPath}/Users`,{method:"POST",headers:{Authorization:Y,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify(Z)});z(G.status===201,`Expected 201 Created, got ${G.status}`);let W=m(await G.json(),"Created SCIM user");r0(W,R5.iam.user);let H=g1(W.schemas,"Created user schemas"),J=v0(W.id,"Created user id"),j=v0(W.userName,"Created user userName");if(v5(G,W,"Users",J),z(H.includes(P2),"Created user missing SCIM User schema"),Q[p1])z(H.includes(p1),"Created user missing enterprise extension schema");if(Q[R0]){z(H.includes(R0),"Created user missing ionite User extension schema");let D=m(W[R0],"Created user ionite extension");z(D.ecid===u9,"Created user must round-trip its ECID"),z(D.accountClass==="daily","Created user must round-trip its accountClass")}return{details:{userId:J,userName:j}}});return{...K,userId:K.details?.userId}}async function i9(X,$,Q){return N("IAM Get User",async()=>{let K=await H0(X),Y=await $(`${X.scimPath}/Users/${Q}`,{headers:{Authorization:K,Accept:"application/scim+json"}});z(Y.status===200,`Expected 200 OK, got ${Y.status}`);let Z=m(await Y.json(),"Fetched SCIM user");r0(Z,R5.iam.user);let G=v0(Z.id,"Fetched user id"),W=v0(Z.userName,"Fetched user userName");z(G===Q,`User ID mismatch: expected ${Q}, got ${G}`);let H=X.testUser?.[p1];if(H){let j=m(Z["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"Fetched user enterprise extension");if(H.department!==void 0)z(j.department===H.department,`Expected department ${H.department}, got ${String(j.department)}`);if(H.costCenter!==void 0)z(j.costCenter===H.costCenter,`Expected costCenter ${H.costCenter}, got ${String(j.costCenter)}`)}let J=X.testUser?.[R0];if(J){let j=m(Z[R0],"Fetched user ionite extension");z(j.ecid===J.ecid,"Fetched user must preserve its ECID"),z(j.accountClass===J.accountClass,"Fetched user must preserve its accountClass")}return{details:{userId:G,userName:W}}})}async function o9(X,$,Q){return N("IAM Update User",async()=>{let K=await H0(X),Y={schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:"ECV Test User (Updated)"}]},Z=await $(`${X.scimPath}/Users/${Q}`,{method:"PATCH",headers:{Authorization:K,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify(Y)});z(Z.status===200,`Expected 200 OK, got ${Z.status}`);let G=m(await Z.json(),"Updated SCIM user"),W=v0(G.id,"Updated user id"),H=v0(G.displayName,"Updated user displayName");return z(H==="ECV Test User (Updated)","displayName was not updated"),{details:{userId:W,displayName:H}}})}async function a9(X,$,Q){return N("IAM Delete User",async()=>{let K=await H0(X),Y=await $(`${X.scimPath}/Users/${Q}`,{method:"DELETE",headers:{Authorization:K}});z(Y.status===204,`Expected 204 No Content, got ${Y.status}`);let Z=await $(`${X.scimPath}/Users/${Q}`,{headers:{Authorization:K,Accept:"application/scim+json"}});return z(Z.status===404,`Expected 404 after deletion, got ${Z.status}`),{details:{userId:Q,deleted:!0}}})}async function t9(X,$){if(!X.testGroup)return{...W1("IAM Create Group","No test group configured"),groupId:void 0};let Q=await N("IAM Create Group",async()=>{let K=await H0(X),Y={schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],...X.testGroup},Z=await $(`${X.scimPath}/Groups`,{method:"POST",headers:{Authorization:K,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify(Y)});z(Z.status===201,`Expected 201 Created, got ${Z.status}`);let G=m(await Z.json(),"Created SCIM group");r0(G,R5.iam.group);let W=v0(G.id,"Created group id"),H=v0(G.displayName,"Created group displayName");return q2(G,"Created SCIM group"),v5(Z,G,"Groups",W),{details:{groupId:W,displayName:H}}});return{...Q,groupId:Q.details?.groupId}}async function r9(X,$,Q){return N("IAM Delete Group",async()=>{let K=await H0(X),Y=await $(`${X.scimPath}/Groups/${Q}`,{method:"DELETE",headers:{Authorization:K}});return z(Y.status===204,`Expected 204 No Content, got ${Y.status}`),{details:{groupId:Q,deleted:!0}}})}async function s9(X,$){return N("IAM Users Endpoint Requires Authentication",async()=>{let Q=await $(`${X.scimPath}/Users`,{headers:{Accept:"application/scim+json"}});return z(Q.status===401,`Expected 401 Unauthorized for Users, got ${Q.status}`),{details:{usersRequiresAuth:!0}}})}async function f8(X,$){return N("IAM Groups Endpoint Requires Authentication",async()=>{let Q=await $(`${X.scimPath}/Groups`,{headers:{Accept:"application/scim+json"}});return z(Q.status===401,`Expected 401 Unauthorized for Groups, got ${Q.status}`),{details:{groupsRequiresAuth:!0}}})}async function e9(X,$){return N("IAM Users Endpoint Denies Unauthorized (403)",async()=>{let Q=await I8(X),K=await $(`${X.scimPath}/Users`,{headers:{Authorization:Q,Accept:"application/scim+json"}});return z(K.status===403,`Expected 403 Forbidden for an authenticated-but-unauthorized token, got ${K.status}`),{details:{deniedByDefault:!0}}})}function $$(X,$){return[{name:"IAM discovery endpoints require authentication",fn:async()=>{for(let Q of["ServiceProviderConfig","ResourceTypes","Schemas"]){let K=await $(`${X.scimPath}/${Q}`,{headers:{Accept:"application/scim+json"}});z(K.status===401,`Expected 401 from unauthenticated ${Q}, got ${K.status}`)}}},{name:"IAM ServiceProviderConfig advertises OAuth bearer auth and filtering",fn:async()=>{let Q=await $(`${X.scimPath}/ServiceProviderConfig`,{headers:{Authorization:await H0(X),Accept:"application/scim+json"}});z(Q.status===200,`Expected 200 from ServiceProviderConfig, got ${Q.status}`);let K=m(await Q.json(),"ServiceProviderConfig"),Y=m(K.filter,"ServiceProviderConfig.filter");z(Y.supported===!0,"ServiceProviderConfig must advertise filter support");let Z=$1(K.authenticationSchemes,"ServiceProviderConfig.authenticationSchemes").map((W)=>m(W,"ServiceProviderConfig authentication scheme"));z(Z.some((W)=>W.type==="oauthbearertoken"),"ServiceProviderConfig must advertise OAuth bearer token authentication");let G=m(K.meta,"ServiceProviderConfig.meta");new URL(v0(G.location,"ServiceProviderConfig.meta.location"))}},{name:"IAM Schemas exposes the ionite ECID User extension",fn:async()=>{let Q=await H0(X),K=await $(`${X.scimPath}/Schemas`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(K.status===200,`Expected 200 from Schemas, got ${K.status}`);let Y=m(await K.json(),"Schemas response");z(g1(Y.schemas,"Schemas response schemas").includes(k8),"Schemas response must be a SCIM ListResponse");let Z=$1(Y.Resources,"Schemas Resources").map((j)=>m(j,"Schema resource"));z(Z.some((j)=>j.id===P2),"Schemas must include the core User schema"),z(Z.some((j)=>j.id===R0),"Schemas must include the ionite User extension");let G=await $(`${X.scimPath}/Schemas/${encodeURIComponent(R0)}`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(G.status===200,`Expected 200 from ionite User Schema, got ${G.status}`);let W=m(await G.json(),"Ionite User Schema");z(W.id===R0,"Schema detail must identify the ionite User extension");let H=$1(W.attributes,"Ionite User Schema attributes").map((j)=>m(j,"Ionite User Schema attribute")),J=H.find((j)=>j.name==="ecid");z(J?.type==="string","Ionite User Schema must define ecid as a string"),z(J.caseExact===!0,"Ionite User Schema must define ecid as case-exact"),z(H.some((j)=>j.name==="accountClass"),"Ionite User Schema must define accountClass")}},{name:"IAM ResourceTypes binds the ionite extension to Users",fn:async()=>{let Q=await $(`${X.scimPath}/ResourceTypes`,{headers:{Authorization:await H0(X),Accept:"application/scim+json"}});z(Q.status===200,`Expected 200 from ResourceTypes, got ${Q.status}`);let K=m(await Q.json(),"ResourceTypes response"),Z=$1(K.Resources,"ResourceTypes Resources").map((W)=>m(W,"ResourceType resource")).find((W)=>W.id==="User");z(Z!==void 0,"ResourceTypes must include User");let G=$1(Z.schemaExtensions,"User schemaExtensions").map((W)=>m(W,"User schema extension"));z(G.some((W)=>W.schema===R0),"User ResourceType must reference the ionite User extension")}},{name:"IAM Users rejects a malformed ECID at the HTTP boundary",fn:async()=>{let Q=await $(`${X.scimPath}/Users`,{method:"POST",headers:{Authorization:await H0(X),"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify({schemas:[P2,R0],userName:`invalid-ecid-${Date.now()}@example.test`,active:!0,[R0]:{ecid:"not-an-ecid",accountClass:"daily"}})});z(Q.status===400,`Expected 400 for malformed ECID, got ${Q.status}`)}}]}async function p8(X){let $=performance.now(),Q={...d9,...X},K=r(Q),Y=[],Z=typeof X.ion==="function"?X.ion():X.ion;if((Z===null||Z===void 0)&&!X.ecvUrl)return{suite:"IAM",passed:!1,tests:[{name:"IAM Configuration",passed:!1,error:"IAM tests require either es (Ionite instance) or ecvUrl (ECV mock server URL)",duration:0}],duration:0,summary:{total:1,passed:0,failed:1,skipped:0}};if(Z&&!Z.workload)return{suite:"IAM",passed:!1,tests:[{name:"IAM Configuration",passed:!1,error:"Workload management is required for IAM tests. Ionite instance provided but workload is not configured.",duration:0}],duration:0,summary:{total:1,passed:0,failed:1,skipped:0}};Y.push(await s9(Q,K)),Y.push(await e9(Q,K));for(let D of $$(Q,K))Y.push(await N(D.name,async()=>{await D.fn();return}));Y.push(await c9(Q,K));let G=await n9(Q,K);if(Y.push(G),G.passed&&G.userId)Y.push(await i9(Q,K,G.userId)),Y.push(await o9(Q,K,G.userId)),Y.push(await a9(Q,K,G.userId));if(X.testGroup){Y.push(await f8(Q,K)),Y.push(await m8(Q,K));let D=await t9(Q,K);if(Y.push(D),D.passed&&D.groupId)Y.push(await r9(Q,K,D.groupId))}let W=performance.now()-$,H=Y.filter((D)=>D.passed).length,J=Y.filter((D)=>!D.passed).length,j=Y.filter((D)=>D.details?.skipped).length;return{suite:"IAM",passed:J===0,tests:Y,duration:W,summary:{total:Y.length,passed:H-j,failed:J,skipped:j}}}function F2(X){let $={...d9,...X},Q=r($),K,Y,Z;return{tests:[{name:"users endpoint requires authentication",fn:async()=>{let H=await s9($,Q);if(!H.passed)throw Error(H.error)}},{name:"users endpoint denies an authenticated-but-unauthorized token (403)",fn:async()=>{let H=await e9($,Q);if(!H.passed)throw Error(H.error)}},{name:"users endpoint returns proper SCIM schema",fn:async()=>{let H=await c9($,Q);if(!H.passed)throw Error(H.error)}},...$$($,Q),{name:"can create a user",fn:async()=>{let H=await n9($,Q);if(!H.passed)throw Error(H.error);K=H.userId}},{name:"can get a user by ID",fn:async()=>{if(!K)throw Error("No user ID from create test");let H=await i9($,Q,K);if(!H.passed)throw Error(H.error)}},{name:"can update a user",fn:async()=>{if(!K)throw Error("No user ID from create test");let H=await o9($,Q,K);if(!H.passed)throw Error(H.error)}},{name:"can delete a user",fn:async()=>{if(!K)throw Error("No user ID from create test");let H=await a9($,Q,K);if(!H.passed)throw Error(H.error)}}],ext:{createGroupsOutboundTests:()=>[{name:"can create a group in external IAM",fn:async()=>{let H=await t9($,Q);if(!H.passed)throw Error(H.error);Y=H.groupId}},{name:"can delete a group in external IAM",fn:async()=>{if(!Y)throw Error("No group ID from create test");let H=await r9($,Q,Y);if(!H.passed)throw Error(H.error)}}],createGroupsInboundTests:()=>{let H=X.groupsInboundPath??`${X.scimPath}/Groups`;return[{name:"inbound endpoint requires authentication",fn:async()=>{let J=await Q(H,{headers:{Accept:"application/scim+json"}});if(J.status!==401)throw Error(`Expected 401 Unauthorized, got ${J.status}`)}},{name:"inbound endpoint returns proper SCIM schema",fn:async()=>{let J=await H0($),j=await Q(H,{headers:{Authorization:J,Accept:"application/scim+json"}});if(j.status!==200)throw Error(`Expected 200 OK, got ${j.status}`);let D=m(await j.json(),"Inbound groups response");if(!g1(D.schemas,"Inbound response schemas").includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"))throw Error("Response missing SCIM ListResponse schema")}},{name:"can receive group creation from IAM provider",fn:async()=>{let J=await H0($),D=await Q(H,{method:"POST",headers:{Authorization:J,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"ECV Inbound Test Group",externalId:"ecv-inbound-test-group-001"})});if(D.status!==201)throw Error(`Expected 201 Created, got ${D.status}`);let P=m(await D.json(),"Created inbound SCIM group"),B=v0(P.id,"Created inbound group id");if(v0(P.displayName,"Created inbound group displayName"),q2(P,"Created inbound SCIM group"),v5(D,P,"Groups",B),!B)throw Error("Created group missing id or displayName");Z=B}},{name:"ServiceProviderConfig advertises filter support (§11.3)",fn:async()=>{let J=await H0($),j=H.replace(/\/Groups$/,""),D=await Q(`${j}/ServiceProviderConfig`,{headers:{Authorization:J,Accept:"application/scim+json"}});if(D.status!==200)throw Error(`Expected 200 OK from ServiceProviderConfig, got ${D.status}`);let P=m(await D.json(),"ServiceProviderConfig");if(m(P.filter,"ServiceProviderConfig.filter").supported!==!0)throw Error("ServiceProviderConfig must advertise filter.supported: true (filtering IS implemented)")}},{name:"externalId eq filter round-trips the created group (§11.3)",fn:async()=>{let J=await H0($),j=encodeURIComponent('externalId eq "ecv-inbound-test-group-001"'),D=await Q(`${H}?filter=${j}`,{headers:{Authorization:J,Accept:"application/scim+json"}});if(D.status!==200)throw Error(`Expected 200 OK from filtered Groups list, got ${D.status}`);let P=m(await D.json(),"Filtered groups response"),q=$1(P.Resources??[],"Filtered Resources").map((M)=>m(M,"Filtered resource")).find((M)=>M.externalId==="ecv-inbound-test-group-001");if(!q)throw Error("externalId eq filter did not return the group created with that externalId");let L=m(q.meta,"Filtered group meta"),O=v0(L.location,"Filtered group meta.location");if(!new URL(O).pathname.endsWith(`/Groups/${encodeURIComponent(v0(q.id,"Group id"))}`))throw Error("Filtered group meta.location does not address its SCIM Group resource");q2(q,"Filtered SCIM group")}},{name:"rejects a malformed PATCH body with 400 (§10.2)",fn:async()=>{if(!Z)throw Error("No group ID from create test");let J=await H0($),j=await Q(`${H}/${Z}`,{method:"PATCH",headers:{Authorization:J,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:12345}]})});if(j.status!==400)throw Error(`Expected 400 for a malformed group PATCH body, got ${j.status}`)}},{name:"can receive group deletion from IAM provider",fn:async()=>{if(!Z)throw Error("No group ID from create test");let J=await H0($),j=await Q(`${H}/${Z}`,{method:"DELETE",headers:{Authorization:J}});if(j.status!==204)throw Error(`Expected 204 No Content, got ${j.status}`)}}]}}}}import{createManagedIamDelta as c0,InMemoryAccessCatalogStore as Y$,InMemoryUserStore as Z$,ION_AUTHZ_GROUP_SCHEMA as x1,ION_SCIM_USER_EXTENSION_URN as Q$,ionite as g8,ScimOutboundClient as u8,silentLogger as h0,workload as K$}from"@ionite/server";import{createValidators as l8}from"@ionite/valibot";var d8="urn:ietf:params:scim:api:messages:2.0:ListResponse";function i(X="https://iam.example.test/scim/v2"){let $=new Map,Q=new Set,K=new Map,Y=new Set,Z=[],G=0,W,H,J=404,j=(q,L)=>new Response(JSON.stringify(L),{status:q,headers:{"Content-Type":"application/scim+json"}}),D=(q,L,O,M={})=>{let F=`${X}/${q}/${encodeURIComponent(L)}`,T=`${new URL(X).origin}/request-access/${encodeURIComponent(L)}`,x=q==="Groups"?{...M[x1]??{},requestUrl:T}:void 0;return{...M,id:L,externalId:O,meta:{resourceType:q==="Groups"?"Group":"User",location:F},...x?{[x1]:x}:{}}},P=(q,L,O)=>j(200,{schemas:[d8],totalResults:O?1:0,Resources:O?[D(q,O,L)]:[]});return{baseUrl:X,fetchImpl:async(q,L)=>{let O=new URL(q),M=(L?.method??"GET").toUpperCase(),F=O.pathname.split("/").filter(Boolean),T=F[F.length-2],x=T==="Groups"||T==="Users"?T:F[F.length-1],b=O.pathname.endsWith("/Users"),p=O.pathname.endsWith("/Groups"),F0=O.searchParams.get("filter")??void 0,k=L?.body?JSON.parse(String(L.body)):void 0;if(Z.push({method:M,path:O.pathname,filter:F0,body:k}),M==="GET"&&(b||p)){if(H!==void 0)return j(H,{detail:"lookup rejected"});let C=F0?.match(/externalId eq "([^"]*)"/i)?.[1];z(C!==void 0,"destination lookups must be by externalId filter only");let X0=(b?$:K).get(C);return P(b?"Users":"Groups",C,X0)}if(M==="POST"&&(b||p)){let C=String(k?.externalId??"");G+=1;let X0=`${b?"u":"g"}_${G}`;if(b)$.set(C,X0),Q.add(X0);else K.set(C,X0),Y.add(X0);return j(201,D(b?"Users":"Groups",X0,C,k))}if(M==="PUT"&&(x==="Users"||x==="Groups")){let C=decodeURIComponent(F[F.length-1]??"");if(!(x==="Users"?Q:Y).has(C))return j(J,{detail:"resource not found"});let u0=String(k?.externalId??"");return(x==="Users"?$:K).set(u0,C),j(200,D(x,C,u0,k))}if(M==="DELETE"&&(x==="Users"||x==="Groups")){let C=decodeURIComponent(F[F.length-1]??""),X0=x==="Users"?Q:Y,u0=x==="Users"?$:K;if(!X0.delete(C))return j(J,{detail:"resource not found"});for(let[J5,Q8]of u0)if(Q8===C)u0.delete(J5);return new Response(null,{status:204})}if(M==="PATCH"&&x==="Groups"){let C=decodeURIComponent(F[F.length-1]??"");if(!Y.has(C))return j(J,{detail:"group not found"});if(W!==void 0)return j(W,{detail:"patch rejected"});return j(200,{id:C})}return j(404,{detail:"not found"})},calls:Z,seedUser(q,L){$.set(q,L),Q.add(L)},seedGroup(q,L){K.set(q,L),Y.add(L)},countCalls(q){return Z.filter(q).length},failGroupPatchWith(q){W=q},failLookupWith(q){H=q},missingResourceWith(q){J=q}}}function X$(){let X=new Map,$=[],Q=[],K=(Y,Z)=>`${Y}\x00${Z}`;return{callbacks:{async read(Y,Z){return X.get(K(Y,Z))},async write(Y,Z,G){X.set(K(Y,Z),G),$.push({resourceType:Y,externalId:Z,reference:G})},async clear(Y,Z){X.delete(K(Y,Z)),Q.push({resourceType:Y,externalId:Z})}},get:(Y,Z)=>X.get(K(Y,Z)),set:(Y,Z,G)=>X.set(K(Y,Z),G),writes:$,clears:Q}}function L0(X,$=X$()){return{client:new u8({baseUrl:X.baseUrl,getToken:async()=>"test-token",receipts:$.callbacks,log:h0,fetchImpl:X.fetchImpl}),receipts:$}}var O0={isReady:()=>!0,ready:async()=>{return},getServerToken:async()=>"test-token",getToken:async(X)=>`token-for-${X??"server"}`};function n0(){return{userStore:new Z$,catalogStore:new Y$}}function c8(X){return{async load(){return X},async subscribe(){return},secret:{}}}function L2(){return{tests:[{name:"resolveProviderId resolves a destination id STRICTLY by externalId filter",fn:async()=>{let $=i();$.seedUser("u-alice","u_42");let{client:Q,receipts:K}=L0($),Y=await Q.resolveProviderId("user","u-alice");z(typeof Y==="object"&&Y.found==="u_42","resolves the destination id returned by the externalId filter lookup");let Z=$.calls.find((W)=>W.method==="GET"&&W.path.endsWith("/Users"));z(Z?.filter?.includes('externalId eq "u-alice"')===!0,"lookup used the externalId filter");let G=K.get("user","u-alice");z(G?.providerId==="u_42","the resolved id is written through to the row receipt")}},{name:"create-on-empty: an unknown externalId is created at the destination",fn:async()=>{let $=i(),{client:Q,receipts:K}=L0($),Y=await Q.resolveProviderId("group","g-new",{displayName:"New Group"});z(typeof Y==="object"&&Y.found.startsWith("g_"),"a missing resource is created and its id returned"),z($.countCalls((G)=>G.method==="POST"&&G.path.endsWith("/Groups"))===1,"exactly one create is issued when the destination has no match");let Z=K.get("group","g-new");z(Z?.providerId===(typeof Y==="object"?Y.found:void 0),"the provider id is cached"),z(Z?.requestUrl?.includes("/request-access/")===!0,"the IAM-owned request URL is captured from the create response")}},{name:"externalId lookup captures the IAM-owned entitlement request URL",fn:async()=>{let $=i();$.seedGroup("g-existing","g_44");let{client:Q,receipts:K}=L0($);await Q.resolveProviderId("group","g-existing");let Y=K.get("group","g-existing");z(Y?.providerId==="g_44","the lookup caches the provider id"),z(Y?.requestUrl?.endsWith("/request-access/g_44")===!0,"the lookup caches the IAM-owned request URL")}},{name:"mapping hit: a cached id avoids a second filter lookup",fn:async()=>{let $=i();$.seedGroup("g-eng","g_7");let{client:Q}=L0($);await Q.resolveProviderId("group","g-eng"),await Q.resolveProviderId("group","g-eng"),z($.countCalls((K)=>K.method==="GET"&&K.path.endsWith("/Groups"))===1,"the second resolve is served from the row receipt/process memo (no extra filter GET)")}},{name:"patchGroupMembership(add) resolves both sides by externalId and PATCHes membership",fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("g-eng","g_1");let{client:Q}=L0($);await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"});let K=$.calls.find((Z)=>Z.method==="PATCH"&&Z.path.endsWith("/Groups/g_1"));z(K!==void 0,"a membership PATCH is issued against the resolved group id");let Y=K?.body?.Operations;z(Y?.[0]?.op==="add","the patch adds membership"),z(Y?.[0]?.value?.[0]?.value==="u_1","membership references the resolved user provider id")}},{name:"stale cached group id self-heals on a destination 404 (invalidate -> re-resolve -> retry)",fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("g-eng","g_real");let{client:Q,receipts:K}=L0($);K.set("group","g-eng",{providerId:"g_stale",requestUrl:"https://old-iam.example.test/request/g_stale"}),await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"}),z($.calls.some((Z)=>Z.method==="PATCH"&&Z.path.endsWith("/Groups/g_stale")),"the first PATCH targets the stale id and is rejected (404)"),z($.calls.some((Z)=>Z.method==="PATCH"&&Z.path.endsWith("/Groups/g_real")),"after self-heal the retry targets the freshly re-resolved id");let Y=K.get("group","g-eng");z(Y?.providerId==="g_real","the row receipt is repaired to the correct provider id"),z(K.clears.some((Z)=>Z.resourceType==="group"&&Z.externalId==="g-eng"),"the stale row receipt is cleared before the externalId re-filter"),z(Y?.requestUrl?.endsWith("/request-access/g_real")===!0,"self-heal refreshes the IAM-owned request URL")}},{name:"idempotency: repeated adds do not create duplicate destination resources",fn:async()=>{let $=i(),{client:Q}=L0($),K={groupExternalId:"g-eng",userExternalId:"u-alice",op:"add",groupCreate:{displayName:"Eng"},userCreate:{userName:"alice"}};await Q.patchGroupMembership(K),await Q.patchGroupMembership(K),z($.countCalls((Y)=>Y.method==="POST"&&Y.path.endsWith("/Users"))===1,"the user is created at most once across repeated emits"),z($.countCalls((Y)=>Y.method==="POST"&&Y.path.endsWith("/Groups"))===1,"the group is created at most once across repeated emits")}},{name:"concurrent full upserts serialize without losing the later body",fn:async()=>{let $=i(),{client:Q}=L0($);await Promise.all([Q.upsertResource("group","g-eng",{displayName:"Engineering v1"}),Q.upsertResource("group","g-eng",{displayName:"Engineering v2"})]),z($.countCalls((Y)=>Y.method==="POST"&&Y.path.endsWith("/Groups"))===1,"concurrent upserts create the destination Group once");let K=$.calls.find((Y)=>Y.method==="PUT"&&Y.path.includes("/Groups/"));z(K?.body?.displayName==="Engineering v2","the later serialized upsert applies its full body")}},{name:"full entity upsert replaces a known Group with its authz extension",fn:async()=>{let $=i();$.seedGroup("g-reviewers","g_10");let{client:Q,receipts:K}=L0($),Y=await Q.upsertResource("group","g-reviewers",{schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],externalId:"g-reviewers",displayName:"Reviewers",[x1]:{type:"group",requestable:!0,requestUrl:"https://app.example.test/must-not-egress",description:"Review access",resources:["project:42"]}});z(Y==="pushed","the full Group upsert is accepted");let G=$.calls.find((H)=>H.method==="PUT"&&H.path.endsWith("/Groups/g_10"))?.body?.[x1];z(G?.requestable===!0,"the requestable flag is included in the PUT body"),z(G?.resources?.[0]==="project:42","resource bindings are included in the PUT body"),z(G?.requestUrl===void 0,"the destination-owned request URL is stripped from the PUT body");let W=K.get("group","g-reviewers");z(W?.requestUrl?.endsWith("/request-access/g_10")===!0,"the destination response refreshes the cached request URL")}},{name:"non-requestable catalog definitions and grants stay out of IAM delta",fn:async()=>{let $=i(),Q={id:"vault_secret_create",name:"Create secret",description:"Internal permission composed into requestable resource Groups",requestable:!1},Y=c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),fetchImpl:$.fetchImpl});await Y.onEntitlementUpsert(Q,"permission"),await Y.onGrant({subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test",name:"Alice",email:"alice@example.test"},subjectType:"user",targetType:"permission",target:Q}),z($.calls.length===0,"non-requestable definitions are Authz/OAA building blocks and never reach IAM delta")}},{name:"entitlement metadata upsert reapplies observed roots after the destination PUT",fn:async()=>{let $=i();$.seedGroup("reviewers","g_10");let K={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,groups:[{id:"employees",name:"Employees",description:"All employees",requestable:!0}]},Y={listByTarget:async()=>({items:[{subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test"},subjectType:"user",targetType:"group",target:K}],total:1,count:1,size:1,page:1,pages:1})};await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),accessStore:Y,fetchImpl:$.fetchImpl}).onEntitlementUpsert(K,"group");let W=$.calls.findIndex((j)=>j.method==="PUT"&&j.path.endsWith("/Groups/g_10")),H=$.calls.findIndex((j)=>j.method==="PATCH"&&j.path.endsWith("/Groups/g_10")),J=$.calls[W]?.body;z(W>=0,"the entitlement definition is replaced"),z(J?.members?.[0]?.value?.startsWith("u_")===!0,"the PUT carries the resolved existing member"),z(J?.members?.[0]?.type==="User","the PUT carries the full-sync member type"),z(J?.members?.some((j)=>j.type==="Group"&&j.value?.startsWith("g_"))===!0,"the PUT carries nested Group membership through destination provider ids"),z(H>W,"existing roots are restored after the definition PUT")}},{name:"entitlement upsert pages through every observed root (members and restore beyond one page)",fn:async()=>{let $=i();$.seedGroup("reviewers","g_10");let Q={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0},K=1002,Y=Array.from({length:K},(P,B)=>({subject:{id:`user-${B}`,externalId:`u-${B}`,userName:`user${B}@example.test`},subjectType:"user",targetType:"group",target:Q})),Z=[],G={listByTarget:async(P,B,q)=>{let L=q?.start??0,O=q?.limit??K;Z.push(L);let M=Y.slice(L,L+O);return{total:K,count:M.length,items:M,size:O,page:Math.floor(L/Math.max(O,1))+1,pages:Math.ceil(K/Math.max(O,1))}}};await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),accessStore:G,fetchImpl:$.fetchImpl}).onEntitlementUpsert(Q,"group"),z(Z.includes(1000),"the emitter requests roots beyond the first page");let j=$.calls.find((P)=>P.method==="PUT"&&P.path.endsWith("/Groups/g_10"))?.body?.members??[];z(j.length===K,"the definition PUT carries members from every page");let D=$.countCalls((P)=>P.method==="PATCH"&&P.path.endsWith("/Groups/g_10"));z(D===K,"the restore loop reapplies every observed root, not just one page")}},{name:"User deactivation and ECID egress through the managed emitter, then delete converges",fn:async()=>{let $=i(),Q=c0(h0),K=n0(),Y=Q.update({mode:"patch",url:$.baseUrl},{workload:O0,...K,fetchImpl:$.fetchImpl}),Z="u-terminated-alice",G={id:"terminated-alice",externalId:"u-terminated-alice",userName:"terminated.alice@example.test",name:"Terminated Alice",email:"terminated.alice@example.test",active:!1,ecid:"ecid:acme.test:01J9F3Z8QK7M2N4P6R8T0V2W4X",accountClass:"daily"};await K.userStore.upsert(G),await Y.onUserUpsert(G);let H=$.calls.find((j)=>j.method==="POST"&&j.path.endsWith("/Users"))?.body;z(H?.active===!1,"a terminated user egresses with active:false"),z(H?.schemas?.includes(Q$)===!0,"the User egress declares the ionite ECID extension schema");let J=H?.[Q$];z(J?.ecid===G.ecid,"the User egress carries the ECID"),z(J?.accountClass===G.accountClass,"the User egress carries accountClass"),await Y.onUserDelete("u-terminated-alice"),z($.calls.some((j)=>j.method==="DELETE"&&j.path.includes("/Users/u_")),"the managed emitter deletes the destination User by its resolved provider id"),z(await Y.getDestinationRef("user","u-terminated-alice")===void 0,"the managed emitter clears the deleted User destination reference")}},{name:"full User upsert creates on empty and delete removes the provider resource and row receipt",fn:async()=>{let $=i(),{client:Q,receipts:K}=L0($),Y=await Q.upsertResource("user","u-alice",{schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],externalId:"u-alice",userName:"alice@example.test",displayName:"Alice",active:!0,emails:[{value:"alice@example.test",primary:!0}]});z(Y==="pushed","an unknown User is created with its full body");let Z=$.calls.find((H)=>H.method==="POST"&&H.path.endsWith("/Users"));z(Z?.body?.emails?.[0]?.value==="alice@example.test","full User attributes are present on create-on-empty");let G=await Q.deleteResource("user","u-alice");z(G==="pushed","the destination User delete is accepted"),z($.calls.some((H)=>H.method==="DELETE"&&H.path.includes("/Users/u_")),"the resolved provider User is deleted");let W=K.get("user","u-alice");z(W===void 0,"the deleted User mapping is removed")}},{name:"deleteResource treats a confirmed absent destination resource as idempotent success",fn:async()=>{let $=i(),{client:Q}=L0($),K=await Q.deleteResource("group","g-absent");z(K==="pushed","confirmed absence is a successful converged delete"),z($.countCalls((Y)=>Y.method==="POST"||Y.method==="DELETE")===0,"an absent delete neither creates nor issues a provider-id DELETE")}},{name:"deleteResource self-heals a stale cached id before declaring convergence",fn:async()=>{let $=i();$.missingResourceWith(410),$.seedGroup("g-reviewers","g_real");let{client:Q,receipts:K}=L0($);K.set("group","g-reviewers",{providerId:"g_stale"});let Y=await Q.deleteResource("group","g-reviewers");z(Y==="pushed","the stale mapping converges to a successful delete"),z($.calls.some((Z)=>Z.method==="DELETE"&&Z.path.endsWith("/Groups/g_stale")),"the first delete detects the stale cached id"),z($.calls.some((Z)=>Z.method==="DELETE"&&Z.path.endsWith("/Groups/g_real")),"the client re-filters externalId and deletes the current provider id")}},{name:"direct role grants egress as membership on an extension-typed SCIM Group",fn:async()=>{let $=i();await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),fetchImpl:$.fetchImpl}).onGrant({subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test"},subjectType:"user",targetType:"role",target:{id:"reviewer",name:"Reviewer",requestable:!0}});let Y=$.calls.find((G)=>G.method==="POST"&&G.path.endsWith("/Groups")),Z=Y?.body?.[x1];z(Z?.type==="role","the role definition is represented as a typed SCIM Group"),z(Y?.body?.schemas?.includes(x1)===!0,"the typed Group extension is declared in schemas"),z(Y?.body?.externalId==="reviewer","the App-owned catalog id is the outbound SCIM externalId"),z($.calls.some((G)=>G.method==="PATCH"&&G.path.includes("/Groups/")),"the role grant produces a membership PATCH")}},{name:"workload grants resolve the workload registry and preserve the Workload member type",fn:async()=>{let $=i();await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,workloadIdentityStore:{get:async(W)=>W==="billing-worker"?{id:W,clientId:"billing-worker-client",externalId:"w-billing-worker",displayName:"Billing worker"}:void 0},...n0(),fetchImpl:$.fetchImpl}).onGrant({subject:"billing-worker",subjectType:"workload",targetType:"role",target:{id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0}});let Y=$.calls.find((W)=>W.method==="POST"&&W.path.endsWith("/Users"));z(Y?.body?.userName==="billing-worker-client","the workload registry supplies the destination principal body");let G=$.calls.find((W)=>W.method==="PATCH")?.body?.Operations?.[0]?.value?.[0];z(G?.type==="Workload","the membership PATCH matches the full-sync Workload type")}},{name:"switching IAM destinations self-heals the same row receipt without destination-qualified mappings",fn:async()=>{let $=i("https://iam-a.example.test/scim/v2"),Q=i("https://iam-b.example.test/scim/v2");$.seedGroup("g-eng","g_at_a"),Q.seedGroup("g-eng","g_at_b");let K=X$();await L0($,K).client.resolveProviderId("group","g-eng"),z(K.get("group","g-eng")?.providerId==="g_at_a","the row initially carries destination A");let Y=await L0(Q,K).client.upsertResource("group","g-eng",{displayName:"Engineering"});z(Y==="pushed","the first write after a destination switch self-heals and succeeds"),z(Q.calls.some((Z)=>Z.method==="PUT"&&Z.path.endsWith("/Groups/g_at_a")),"the new client first observes the stale row receipt"),z(Q.calls.some((Z)=>Z.method==="GET"&&Z.path.endsWith("/Groups")&&Z.filter?.includes('externalId eq "g-eng"')),"a stale destination receipt is refreshed strictly by externalId"),z(Q.calls.some((Z)=>Z.method==="PUT"&&Z.path.endsWith("/Groups/g_at_b")),"the write retries once against destination B provider id"),z(K.get("group","g-eng")?.providerId==="g_at_b","the same row now carries destination B")}},{name:"receipt-only row write-back does not recursively egress, while deprecated updates do",fn:async()=>{let $=i(),Q=new Y$,K=new Z$,Y=globalThis.fetch;globalThis.fetch=async(G,W)=>{let H=typeof G==="string"?G:G instanceof URL?G.toString():G.url;if(H==="https://idp.example.test/token")return new Response(JSON.stringify({access_token:"test-token",token_type:"Bearer",expires_in:3600}),{status:200,headers:{"Content-Type":"application/json"}});return $.fetchImpl(H,W)};let Z=g8(c8({id:"ecv-delta-row-receipts",workload:{outgoing:{iam:{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1}}},iam:{url:$.baseUrl,delta:"patch",deltaAudience:"iam"}}),{validators:l8(),iam:{userStore:K},stores:{catalogStore:Q}});try{await Z.ready(1000),z(Z.iam?.delta?.isReady()===!0,"managed delta is ready with both receipt row stores"),await Q.groups.set({id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,source:"app",metadata:{first:1,second:2}}).ready();for(let J=0;J<100;J++){if((await Q.groups.get("reviewers"))?.providerId)break;await new Promise((j)=>setTimeout(j,5))}let G=await Q.groups.get("reviewers");z(G?.providerId?.startsWith("g_")===!0,"destination receipt is written onto the Group row"),z(G?.requestUrl?.includes("/request-access/")===!0,"destination request URL is written onto the Group row");let W=$.countCalls((J)=>(J.method==="POST"||J.method==="PUT")&&J.path.includes("/Groups"));z(W===1,"the receipt-only catalog set is suppressed instead of recursively issuing another entitlement upsert");let H={...G,metadata:{second:2,first:1},requestUrl:`${G?.requestUrl}?verified=true`};await Q.groups.set(H).ready(),await new Promise((J)=>setTimeout(J,20)),z($.countCalls((J)=>(J.method==="POST"||J.method==="PUT")&&J.path.includes("/Groups"))===1,"a receipt-only update is suppressed even when object keys are reconstructed in a different order"),await Q.groups.set({...H,deprecated:!0}).ready();for(let J=0;J<100;J++){if($.countCalls((j)=>(j.method==="POST"||j.method==="PUT")&&j.path.includes("/Groups"))>=2)break;await new Promise((j)=>setTimeout(j,5))}z($.countCalls((J)=>(J.method==="POST"||J.method==="PUT")&&J.path.includes("/Groups"))===2,"deprecated:true is a substantive row update and egresses exactly once")}finally{Z.close?.(),globalThis.fetch=Y}}},{name:"reconcile pushes definitions and users before their access roots",fn:async()=>{let $=i(),Q=c0(h0),K=n0(),Y=Q.update({mode:"patch",url:$.baseUrl},{workload:O0,...K,fetchImpl:$.fetchImpl}),Z={id:"alice",externalId:"u-alice",userName:"alice@example.test",name:"Alice",email:"alice@example.test"},G={id:"eng",name:"Engineering",description:"Engineering access",requestable:!0};await K.userStore.upsert(Z),await K.catalogStore.groups.set(G).ready();let W=await Y.reconcile({groups:[G],users:[Z],access:[{subject:Z,subjectType:"user",targetType:"group",target:G}]});z(W.pushed===3,"definition, user, and access root are all counted as pushed");let H=await K.catalogStore.groups.get("eng"),J=await K.userStore.get("alice");z(H?.providerId?.startsWith("g_")===!0,"first Group resolution persists providerId on row"),z(H?.requestUrl?.includes("/request-access/")===!0,"first Group resolution persists IAM requestUrl on row"),z(J?.providerId?.startsWith("u_")===!0,"first User resolution persists providerId on row");let j=$.calls.findIndex((O)=>O.method==="POST"&&O.path.endsWith("/Groups")),D=$.calls.findIndex((O)=>O.method==="POST"&&O.path.endsWith("/Users")),P=$.calls.findIndex((O)=>O.method==="PATCH");z(j>=0&&j<P,"Group definition is pushed before membership"),z(D>=0&&D<P,"User definition is pushed before membership");let B=await Y.getDestinationRef("group","eng");z(B?.providerId.startsWith("g_")===!0,"the emitter exposes the captured provider id"),z(B?.requestUrl?.includes("/request-access/")===!0,"the emitter exposes the IAM-owned request URL without another network lookup");let q=$.countCalls((O)=>O.method==="GET"),L=await Y.reconcile({groups:[G],users:[Z]});z(L.errors.length===0,"later full-body pushes succeed using persisted row receipts"),z($.countCalls((O)=>O.method==="GET")===q,"later Group and User pushes reuse row receipts without another externalId lookup")}},{name:"readiness: patch needs url + workload + UserStore + AccessCatalogStore",fn:async()=>{let $=c0(h0),Q=n0(),K=$.update({mode:"patch"},{workload:O0,...Q});z(!K.isReady(),"'patch' without a destination url is not ready");let Y=await K.reconcile({});z(Y.errors[0]==="iam delta not ready: destination URL is not configured","an attempted operation reports the failed readiness precondition");let Z=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},Q);z(!Z.isReady(),"'patch' without a workload identity is not ready");let G=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,catalogStore:Q.catalogStore});z(!G.isReady(),"'patch' without a UserStore cannot persist User receipts");let W=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,userStore:Q.userStore});z(!W.isReady(),"'patch' without an AccessCatalogStore cannot persist Group receipts");let H=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,...Q});z(H.isReady(),"'patch' with url + workload + both receipt row stores is ready"),await H.ready(1000);let J=K$(void 0,h0,{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1});z(typeof J?.isReady==="function"&&typeof J.ready==="function","the raw configured workload runtime exposes readiness"),z(J.isReady(),"the raw configured workload runtime is ready"),await J.ready(1000);let j=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:J,...Q});z(j.isReady(),"IAM delta accepts the workload runtime built during config apply");let D=K$(void 0,h0,{outgoing:{iam:{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1}}});z(typeof D?.isReady==="function"&&typeof D.ready==="function"&&"iam"in D,"the aggregate workload runtime exposes module and named-client readiness"),z(D.isReady(),"the aggregate workload runtime is ready");let P=$.update({mode:"patch",url:"https://iam.example.test/scim/v2",audience:"iam"},{workload:D,...Q});z(P.isReady(),"IAM delta accepts the aggregate workload runtime and named outgoing client used by vault tenants");let B=$.update({mode:void 0,url:"https://iam.example.test/scim/v2"},{workload:O0,...Q});z(!B.isReady(),"removing delta mode de-configures the stable handle");let q=!1;try{await B.ready(1)}catch{q=!0}z(q,"ready() re-arms while IAM delta is de-configured"),$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,...Q}),await B.ready(1000)}},{name:"emitter skips an access root whose subject or target cannot be resolved",fn:async()=>{let Q=c0(h0).update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,...n0()});z(Q.isReady(),"emitter is ready for the reconcile path");async function*K(){yield{subject:"alice",subjectType:"user",targetType:"group",target:"eng",status:"active"}}let Y=await Q.reconcile({access:K()});z(Y.pushed===0,"an unresolvable subject/target is not pushed"),z(Y.skipped===1,"the unresolved root is skipped"),z(Y.errors.length===0,"a skip is not an error")}},{name:'patchGroupMembership reports "error" when the destination rejects the PATCH (no false success)',fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("g-eng","g_1"),$.failGroupPatchWith(500);let{client:Q}=L0($),K=await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"});z(K==="error",'a non-2xx destination PATCH is reported as an error, not "pushed"')}},{name:'patchGroupMembership reports "error" (never a create) when the externalId lookup fails',fn:async()=>{let $=i();$.failLookupWith(500);let{client:Q}=L0($),K=await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add",groupCreate:{displayName:"Eng"},userCreate:{userName:"alice"}});z(K==="error",'a failed lookup is reported as an error, not "skipped" or "pushed"'),z($.countCalls((Y)=>Y.method==="POST")===0,"a failed lookup must NOT fall through to create-on-empty (no POST issued)")}},{name:"reconcile surfaces a failing destination PATCH as a reconcile error",fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("eng","g_1"),$.failGroupPatchWith(503);let K=c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),fetchImpl:$.fetchImpl});async function*Y(){yield{subject:{id:"alice",externalId:"u-alice",userName:"alice"},subjectType:"user",targetType:"group",target:{id:"eng",displayName:"Eng",requestable:!0},status:"active"}}let Z=await K.reconcile({access:Y()});z(Z.pushed===0,"a rejected destination push is not counted as pushed"),z(Z.errors.length>0,"a rejected destination PATCH is recorded as a reconcile error")}}]}}async function n8(){let X=performance.now(),$=[],Q=[...L2().tests];for(let G of Q)$.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=$.filter((G)=>!G.passed).length,Z=$.filter((G)=>G.details?.skipped).length;return{suite:"IAM Delta",passed:Y===0,tests:$,duration:K,summary:{total:$.length,passed:$.length-Y-Z,failed:Y,skipped:Z}}}import{ionite as D0}from"@ionite/server";import{createValidators as B0}from"@ionite/valibot";var w1=(X)=>new Promise(($)=>setTimeout($,X));async function C0(X,$=1500){let Q=Date.now()+$;while(Date.now()<Q){if(X())return!0;await w1(10)}return X()}function P0(X,$,Q={}){let K=X,Y,Z=0,G=new Map(Object.entries($)),W=new Map,H=new Map,J=new Map,j=new Map,D=[],P=[],B=[],q=new Set,L=(F)=>{let T=G.get(F);if(T===void 0)throw Error(`Missing test secret ${F}`);return{data:structuredClone(T),metadata:{path:F,version:Z,created:new Date(0)}}},O=(F)=>{for(let T of W.get(F)??[])T(L(F))};return{secret:{type:"dev",isReady:()=>!0,ready:async()=>{},isAvailable:()=>!0,get:()=>{throw Error("not implemented")},getFullSecret:async(F)=>{return J.set(F,(J.get(F)??0)+1),L(F)},getSecret:async(F)=>{return J.set(F,(J.get(F)??0)+1),L(F).data},putSecret:async()=>{throw Error("not implemented")},deleteSecret:async()=>{throw Error("not implemented")},listPaths:async()=>[],exists:async(F)=>G.has(F),requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async(F,T)=>{return D.push({path:F,request:{...T}}),{parked:!0,disposition:T.status===403||T.status===401?"suspect":"cooldown"}},addKmsKeys:async(F,T)=>{return P.push({path:F,request:{keys:T.keys.map((x)=>({...x}))}}),{keys:T.keys.map((x,b)=>({id:x.id??`issued-${b+1}`}))}},revokeKmsKey:async(F,T)=>{return B.push({path:F,request:{...T}}),{revoked:!0}},...Q.events?{subscribeEvents:(F)=>{return q.add(F),()=>q.delete(F)}}:{},getMetadata:async()=>({}),subscribe:async(F,T,x)=>{H.set(F,(H.get(F)??0)+1),j.set(F,{...x?.kmsSlots===void 0?{}:{kmsSlots:x.kmsSlots}});let b=W.get(F)??new Set;if(b.add(T),W.set(F,b),!Q.delayInitialDelivery)T(L(F));return()=>b.delete(T)}},async load(){return K},async subscribe(F){return Y=F,()=>{Y=void 0}},pushConfig(F){K=F,Y?.(F)},pushPool(F,T){Z+=1,G.set(F,structuredClone(T)),O(F)},deliverPool(F){O(F)},subscriptionCount(F){return H.get(F)??0},fetchCount(F){return J.get(F)??0},lastSubscribeOptions(F){return j.get(F)},parkCalls(){return D},issueCalls(){return P},revokeCalls(){return B},emitEvent(F){for(let T of q)T(F)}}}function q0(X,$,Q){return{kind:"kms",slots:[X],keys:[{id:X,state:"active",value:$,notBefore:new Date(0).toISOString()}],...Q}}async function i0(X,$){let Q=!1;try{await X}catch{Q=!0}z(Q,$)}function V2(){return{tests:[{name:"kms selects the active key and preserves graceful-rotation overlap until removal or revocation",fn:async()=>{let $=P0({id:"kms-rotation",kms:{rotation:{path:"/kms/rotation"}}},{["/kms/rotation"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("rotation");z(K?.isReady()===!0,"Expected configured KMS pool to become ready"),w(await K.value(),"token-1","Expected value() to select the declared active key"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k2"],keys:[{id:"k1",state:"deprecated",value:"token-1",notAfter:new Date(Date.now()+60000).toISOString()},{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),w(await K.value(),"token-2","Expected rotation to select the new active key"),z(K.accepts("token-1"),"Expected the deprecated overlap key to remain accepted"),z(K.accepts("token-2"),"Expected the new active key to be accepted"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k2"],keys:[{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),z(!K.accepts("token-1"),"Expected a removed overlap key to stop being accepted immediately"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k3"],keys:[{id:"k2",state:"deprecated",value:"token-2",notAfter:new Date(Date.now()+60000).toISOString()},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),z(K.accepts("token-2"),"Expected the second graceful overlap key to remain accepted"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k3"],keys:[{id:"k2",state:"revoked"},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),z(!K.accepts("token-2"),"Expected a revoked overlap key to stop being accepted immediately"),w(await K.value(),"token-3","Expected revocation not to disturb the current active key")}finally{Q.close()}}},{name:"kms hard replacement drops the old key immediately",fn:async()=>{let $=P0({id:"kms-hard-replacement",kms:{hard:{path:"/kms/hard-replacement"}}},{["/kms/hard-replacement"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("hard");z(K?.isReady()===!0,"Expected the initial hard-replacement pool grant"),z(K.accepts("token-1"),"Expected the initial key to be accepted"),$.pushPool("/kms/hard-replacement",q0("k2","token-2")),w(await K.value(),"token-2","Expected hard replacement to select the replacement key"),z(!K.accepts("token-1"),"Expected hard replacement to drop the old key immediately"),z(K.accepts("token-2"),"Expected hard replacement to accept the replacement key")}finally{Q.close()}}},{name:"kms empty grants re-arm value waiting and a later grant resolves the pending call",fn:async()=>{let $=P0({id:"kms-empty-grant",kms:{empty:{path:"/kms/empty-grant"}}},{["/kms/empty-grant"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("empty");z(K?.isReady()===!0,"Expected the initial pool grant"),$.pushPool("/kms/empty-grant",{kind:"kms",keys:[]}),z(await C0(()=>!K.isReady()),"Expected an empty grant to make the pool not ready"),z(!K.accepts("token-1"),"Expected an empty grant to revoke the cached key"),await i0(K.value(30),"Expected value(timeout) to reject when the empty grant is not replaced");let Y=!1,Z=K.value().then((G)=>{return Y=!0,G});await w1(20),z(!Y,"Expected value() to remain pending while the grant is empty"),$.pushPool("/kms/empty-grant",q0("k2","token-2")),w(await Z,"token-2","Expected a later grant to resolve the already-pending value()"),z(K.isReady(),"Expected the pool to become ready after the later grant")}finally{Q.close()}}},{name:"kms concurrent first value calls wait for initial delivery through one source subscription",fn:async()=>{let $=P0({id:"kms-delayed-initial",kms:{delayed:{path:"/kms/delayed-initial"}}},{["/kms/delayed-initial"]:q0("k1","token-1")},{delayInitialDelivery:!0}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("delayed");z(K!==void 0,"Expected the delayed pool handle"),z(!K.isReady(),"Expected the pool to wait for its initial grant delivery");let Y=Array.from({length:8},()=>K.value());z(Y.every((G)=>G===Y[0]),"Expected all concurrent value() calls to share the same pending waiter");let Z=!1;Y[0]?.then(()=>{Z=!0}),await w1(20),z(!Z,"Expected the first value() to wait before initial delivery"),w($.subscriptionCount("/kms/delayed-initial"),1,"Expected all concurrent callers to share one source subscription"),w($.fetchCount("/kms/delayed-initial"),0,"Expected concurrent value() calls not to issue duplicate fetches"),$.deliverPool("/kms/delayed-initial"),w((await Promise.all(Y)).join(","),Array.from({length:8},()=>"token-1").join(","),"Expected the initial delivery to resolve every concurrent caller with the same grant"),w($.subscriptionCount("/kms/delayed-initial"),1,"Expected initial delivery not to create a new subscription"),w($.fetchCount("/kms/delayed-initial"),0,"Expected initial delivery to avoid one-off source fetches")}finally{Q.close()}}},{name:"kms cached repeated value calls create no additional source calls",fn:async()=>{let $=P0({id:"kms-cached",kms:{cached:{path:"/kms/cached"}}},{["/kms/cached"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("cached");z(K?.isReady()===!0,"Expected the cached pool grant");let Y=$.subscriptionCount("/kms/cached"),Z=$.fetchCount("/kms/cached"),G=await Promise.all(Array.from({length:20},()=>K.value()));z(G.every((W)=>W==="token-1"),"Expected every cached call to return the active key"),w($.subscriptionCount("/kms/cached"),Y,"Expected cached value() calls not to add subscriptions"),w($.fetchCount("/kms/cached"),Z,"Expected cached value() calls not to fetch")}finally{Q.close()}}},{name:"kms value waits for a future notBefore boundary",fn:async()=>{let $=P0({id:"kms-not-before",kms:{scheduled:{path:"/kms/not-before"}}},{["/kms/not-before"]:q0("initial","initial-token")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("scheduled");z(K!==void 0,"Expected the scheduled pool handle"),z(K.isReady(),"Expected the initial pool grant");let Y=new Date(Date.now()+150).toISOString();$.pushPool("/kms/not-before",q0("future","future-token",{keys:[{id:"future",state:"active",value:"future-token",notBefore:Y}]})),z(!K.isReady(),"Expected the future notBefore key to remain unavailable initially");let Z=!1,G=K.value(1000).then((W)=>{return Z=!0,W});await w1(30),z(!Z,"Expected value() to wait before the notBefore boundary"),w(await G,"future-token","Expected value() to resolve when notBefore becomes current"),z(K.isReady(),"Expected the pool to become ready at the notBefore boundary")}finally{Q.close()}}},{name:"kms de-configuration rejects a pending value and re-arms the stable pool handle",fn:async()=>{let $=P0({id:"kms-rearm",kms:{rearm:{path:"/kms/rearm"}}},{["/kms/rearm"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("rearm");z(K?.isReady()===!0,"Expected the initial pool grant"),$.pushPool("/kms/rearm",{kind:"kms",keys:[]}),z(await C0(()=>!K.isReady()),"Expected the empty grant to re-arm value waiting");let Y=K.value().then(()=>"resolved",()=>"rejected");$.pushConfig({id:"kms-rearm"}),z(await C0(()=>!K.isReady()),"Expected the pool to remain not ready after removal"),w(await Promise.race([Y,w1(300).then(()=>"timed-out")]),"rejected","Expected de-configuration to reject the pending value() call"),await i0(K.ready(30),"Expected the re-armed pool ready(timeout) to reject while de-configured");let Z=!1,G=K.ready().then(()=>{Z=!0});await w1(20),z(!Z,"Expected the re-armed ready() waiter to stay pending while de-configured"),$.pushPool("/kms/rearm",q0("k2","token-2")),$.pushConfig({id:"kms-rearm",kms:{rearm:{path:"/kms/rearm"}}}),z(await C0(()=>K.isReady()),"Expected the same stable pool handle to recover after re-configuration"),await G,z(Z,"Expected the re-armed ready() waiter to resolve after re-configuration"),w(await K.value(),"token-2","Expected the restored handle to expose the later grant")}finally{Q.close()}}},{name:"kms wrong document kind and shape fail closed until value timeout or a valid grant",fn:async()=>{let $=P0({id:"kms-invalid",kms:{invalid:{path:"/kms/invalid"}}},{["/kms/invalid"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("invalid");z(K?.isReady()===!0,"Expected the initial valid grant"),$.pushPool("/kms/invalid",{kind:"token",slots:["wrong-kind"],keys:[{id:"wrong-kind",state:"active",value:"wrong-token"}]}),z(await C0(()=>!K.isReady()),"Expected a wrong document kind to fail closed"),z(!K.accepts("token-1"),"Expected invalid delivery to clear previously accepted keys");let Y=performance.now();await i0(K.value(40),"Expected value(timeout) to reject while a wrong-kind document remains current"),z(performance.now()-Y>=20,"Expected wrong-kind value() to wait for its timeout instead of rejecting immediately"),$.pushPool("/kms/invalid",{kind:"kms",slots:["missing"],keys:[]}),z(await C0(()=>!K.isReady()),"Expected an invalid KMS document shape to fail closed");let Z=performance.now();await i0(K.value(40),"Expected value(timeout) to reject while an invalid-shape document remains current"),z(performance.now()-Z>=20,"Expected invalid-shape value() to wait for its timeout instead of rejecting immediately"),$.pushPool("/kms/invalid",q0("k2","token-2")),w(await K.value(100),"token-2","Expected a later valid grant to recover the failed-closed pool")}finally{Q.close()}}},{name:"otel apiKey and apiKeyPool are rejected when configured together",fn:async()=>{let $=P0({id:"kms-conflict",kms:{conflict:{path:"/kms/conflict"}},otel:{name:"kms-conflict",apiKey:"static",apiKeyPool:"conflict",traces:!1,metrics:!1,logs:!1}},{["/kms/conflict"]:q0("k1","managed")}),Q=D0($,{validators:B0()});try{await i0(Q.ready(500),"Expected mutually exclusive OTel credential sources to reject config apply"),z(Q.configStatus.lastError!==void 0,"Expected configStatus.lastError for invalid reference")}finally{Q.close()}}},{name:"kms unique slots stay distinct, alias slot 0, and raise demand on one resubscribe",fn:async()=>{let $=P0({id:"kms-unique",kms:{unique:{path:"/kms/unique"}}},{["/kms/unique"]:{kind:"kms",slots:["k1"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()}]}}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("unique");z(K!==void 0,"Expected the unique pool handle"),w($.lastSubscribeOptions("/kms/unique")?.kmsSlots,1,"Expected the default subscribe demand to be 1"),w(K.current(),"token-1","Expected current() to read the cached slot-0 grant");let Y=K.unique(3),Z=K.unique(3);z(Y.length===3&&Y.every((H,J)=>H===Z[J]),"Expected repeated unique(n) calls to return the same slot handles"),z(Y[0]!==K,"Expected unique() slot handles to be distinct from the pool object"),w(await Y[0]?.value(),"token-1","Expected unique()[0] to alias the pool slot-0 grant"),z(Y[1]?.isReady()===!1,"Expected unfilled slot 1 to stay not ready"),z(Y[2]?.isReady()===!1,"Expected unfilled slot 2 to stay not ready"),w(K.current(),"token-1","Expected raising demand not to clear the slot-0 cache"),await i0(Y[1]?.ready(30),"Expected unfilled slot ready(timeout) to reject"),w($.subscriptionCount("/kms/unique"),2,"Expected unique(3) to resubscribe exactly once"),w($.lastSubscribeOptions("/kms/unique")?.kmsSlots,3,"Expected the resubscribe to carry demand 3");let G=!1,W=Y[1]?.value().then((H)=>{return G=!0,H});await w1(20),z(!G,"Expected an unfilled unique() slot to wait for a later grant"),$.pushPool("/kms/unique",{kind:"kms",slots:["k1","k2","k3"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()},{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),w(await W,"token-2","Expected a later multi-slot grant to fill waiting slots"),w(await Y[2]?.value(),"token-3","Expected slot 2 to receive a distinct key"),w(await K.value(),"token-1","Expected pool.value() to keep aliasing slot 0"),z(K.accepts("token-1")&&K.accepts("token-2")&&K.accepts("token-3"),"Expected accepts() to span every delivered slot"),w(Y[1]?.current(),"token-2","Expected slot current() after the grant"),w($.subscriptionCount("/kms/unique"),2,"Expected later unique(2) not to resubscribe after demand 3"),z(K.unique(2)[0]===Y[0]&&K.unique(2)[1]===Y[1],"Expected a later smaller unique(n) to reuse the existing handles")}finally{Q.close()}}},{name:"kms current() is undefined before a grant and after de-configuration",fn:async()=>{let $=P0({id:"kms-current",kms:{current:{path:"/kms/current"}}},{["/kms/current"]:q0("k1","token-1")},{delayInitialDelivery:!0}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("current");z(K!==void 0,"Expected the current() pool handle"),z(K.current()===void 0,"Expected current() to be undefined before the first grant"),$.deliverPool("/kms/current"),z(await C0(()=>K.current()==="token-1"),"Expected current() after grant delivery"),$.pushConfig({id:"kms-current"}),z(await C0(()=>K.current()===void 0),"Expected current() to clear on de-configuration")}finally{Q.close()}}},{name:"kms park() resolves the used value to a key id and never sends the secret",fn:async()=>{let $=P0({id:"kms-park",kms:{park:{path:"/kms/park"}}},{["/kms/park"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("park");z(K!==void 0,"Expected the park pool handle"),z(await C0(()=>K.current()==="token-1"),"Expected the grant before park");let Y=await K.park({value:"token-1",status:429,reason:"rate limited"});w(Y.parked,!0,"Expected a successful park"),w(Y.disposition,"cooldown","Expected 429 to map to cooldown on the fake source"),w($.parkCalls().length,1,"Expected one outbound park"),w($.parkCalls()[0]?.path,"/kms/park","Expected park to use the bound path"),w($.parkCalls()[0]?.request.keyId,"k1","Expected value→id resolution"),w($.parkCalls()[0]?.request.status,429,"Expected status to be forwarded"),w($.parkCalls()[0]?.request.reason,"rate limited","Expected reason to be forwarded"),z(!JSON.stringify($.parkCalls()[0]?.request).includes("token-1"),"Expected the used value never to appear on the outbound request");let Z=await K.park({value:"already-rotated"});w(Z.parked,!1,"Expected a stale value not to park"),w(Z.stale,!0,"Expected stale to be marked"),w($.parkCalls().length,1,"Expected a stale value not to call the source");let W=await K.unique(1)[0]?.park({value:"token-1",status:403});w(W?.parked,!0,"Expected slot.park to alias the pool"),w($.parkCalls()[1]?.request.keyId,"k1","Expected slot park to resolve the same key"),w($.parkCalls()[1]?.request.status,403,"Expected 403 to be forwarded from the slot alias")}finally{Q.close()}}},{name:"kms park() fails fast when the pool is not configured",fn:async()=>{let $=P0({id:"kms-park-unconfigured",kms:{park:{path:"/kms/park-unconfigured"}}},{["/kms/park-unconfigured"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("park");z(K!==void 0,"Expected the park pool handle"),z(await C0(()=>K.current()==="token-1"),"Expected the grant before park");let Y="";try{await K.park({value:""})}catch(G){Y=G instanceof Error?G.message:String(G)}z(Y.includes("requires a non-empty value"),"Expected empty-value park() to fail fast on the configured handle"),w($.parkCalls().length,0,"Expected empty-value park() not to call the source"),$.pushConfig({id:"kms-park-unconfigured"}),z(await C0(()=>!K.isReady()),"Expected the pool handle to tombstone");let Z="";try{await K.park({value:"token-1"})}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("is not configured"),"Expected tombstoned park() to reject on the SDK handle"),w($.parkCalls().length,0,"Expected tombstoned park() not to call the source")}finally{Q.close()}}},{name:"kms issuer addKeys and revokeKey forward and fail fast when unconfigured or empty",fn:async()=>{let $=P0({id:"kms-issuer",kms:{issuer:{path:"/kms/issuer"}}},{["/kms/issuer"]:{kind:"kms",keys:[]}}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.issuer("issuer");z(K!==void 0,"Expected the issuer handle"),z(K.isReady(),"Expected issuer readiness to be independent of slot grants");let Y=await K.addKeys([{value:"oem-token"}]);w(Y.keys[0]?.id,"issued-1","Expected the source to assign a key id"),w($.issueCalls().length,1,"Expected one outbound addKeys"),w($.issueCalls()[0]?.request.keys[0]?.value,"oem-token","Expected the issued value");let Z=await K.revokeKey({keyId:"issued-1",reason:"compromise"});w(Z.revoked,!0,"Expected revoke to succeed"),w($.revokeCalls()[0]?.request.keyId,"issued-1","Expected revoke to forward the key id"),await i0(K.addKeys([]),"Expected empty addKeys() to fail fast"),await i0(K.revokeKey({keyId:""}),"Expected empty-keyId revokeKey() to fail fast"),w($.issueCalls().length,1,"Expected empty addKeys() not to call the source"),w($.revokeCalls().length,1,"Expected empty revokeKey() not to call the source"),$.pushConfig({id:"kms-issuer"}),z(await C0(()=>!K.isReady()),"Expected issuer handle to tombstone"),await i0(K.addKeys([{value:"oem-token"}]),"Expected tombstoned addKeys() to reject"),w($.issueCalls().length,1,"Expected tombstoned addKeys() not to call the source")}finally{Q.close()}}},{name:"kms issuer events() filters kms_* events and errors when the transport lacks subscribeEvents",fn:async()=>{let $=P0({id:"kms-issuer-events",kms:{events:{path:"/kms/issuer-events"}}},{["/kms/issuer-events"]:{kind:"kms",keys:[]}},{events:!0}),Q=D0($,{validators:B0()});try{await Q.ready(500);let Z=Q.kms?.issuer("events");z(Z!==void 0,"Expected the issuer handle");let G=[],W=Z.events((H)=>{G.push(`${H.event}:${H.keyId??""}`)});$.emitEvent({path:"/kms/issuer-events",event:"updated"}),$.emitEvent({path:"/other",event:"kms_key_parked",keyId:"other"}),$.emitEvent({path:"/kms/issuer-events",event:"kms_key_parked",keyId:"k1"}),$.emitEvent({path:"/kms/issuer-events",event:"kms_pool_low"}),w(G.join(","),"kms_key_parked:k1,kms_pool_low:","Expected filtered issuer events"),W()}finally{Q.close()}let K=P0({id:"kms-issuer-no-events",kms:{events:{path:"/kms/issuer-events"}}},{["/kms/issuer-events"]:{kind:"kms",keys:[]}}),Y=D0(K,{validators:B0()});try{await Y.ready(500);let Z=Y.kms?.issuer("events");z(Z!==void 0,"Expected the issuer handle");let G="";try{Z.events(()=>{})}catch(W){G=W instanceof Error?W.message:String(W)}z(G.includes("events are not supported by this secrets transport"),"Expected events() to fail when the source lacks subscribeEvents")}finally{Y.close()}}},{name:"kms verifier grants parse without slots, accept keys, and reject value/unique",fn:async()=>{let $=P0({id:"kms-verifier",kms:{verifier:{path:"/kms/verifier"}}},{["/kms/verifier"]:{kind:"kms",keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()},{id:"k2",state:"deprecated",value:"token-2",notAfter:new Date(Date.now()+60000).toISOString()}]}}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("verifier");z(K!==void 0,"Expected the verifier pool handle"),z(await C0(()=>K.isReady()),"Expected a verifier grant with usable keys to be ready"),z(K.accepts("token-1"),"Expected the active verifier key to be accepted"),z(K.accepts("token-2"),"Expected the overlap-deprecated verifier key to be accepted"),z(!K.accepts("token-other"),"Expected an unknown value to be rejected"),z(K.current()===void 0,"Expected current() to stay undefined on a verifier grant"),await i0(K.value(30),"Expected value() to reject on a verifier grant");let Y="";try{K.unique(1)}catch(Z){Y=Z instanceof Error?Z.message:String(Z)}z(Y.includes("verifier grant has no slots"),"Expected unique() to reject")}finally{Q.close()}}}]}}async function i8(){let X=performance.now(),$=[];for(let Y of V2().tests)$.push(await N(Y.name,async()=>{await Y.fn();return}));let Q=$.filter((Y)=>!Y.passed).length,K=$.filter((Y)=>Y.details?.skipped).length;return{suite:"KMS",passed:Q===0,tests:$,duration:performance.now()-X,summary:{total:$.length,passed:$.length-Q-K,failed:Q,skipped:K}}}var Q1="0.0.23-beta.20260904.2";var U5=">=0.0.23-beta.20260809.2 <0.1.0",G$=[];function z$(X){let $=X.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);return z($!==null,`Invalid semantic version: ${X}`),{major:Number($[1]),minor:Number($[2]),patch:Number($[3]),prerelease:$[4]?$[4].split(".").map((Q)=>/^\d+$/.test(Q)?Number(Q):Q):[]}}function o8(X,$){if(X===$)return 0;if(typeof X==="number"&&typeof $==="string")return-1;if(typeof X==="string"&&typeof $==="number")return 1;return X<$?-1:1}function a8(X,$){let Q=z$(X),K=z$($);for(let Z of["major","minor","patch"])if(Q[Z]!==K[Z])return Q[Z]<K[Z]?-1:1;if(Q.prerelease.length===0&&K.prerelease.length===0)return 0;if(Q.prerelease.length===0)return 1;if(K.prerelease.length===0)return-1;let Y=Math.max(Q.prerelease.length,K.prerelease.length);for(let Z=0;Z<Y;Z++){let G=Q.prerelease[Z],W=K.prerelease[Z];if(G===void 0)return-1;if(W===void 0)return 1;let H=o8(G,W);if(H!==0)return H}return 0}function t8(X,$){let Q=$.match(/^(>=|<=|>|<|=)?(.+)$/);z(Q!==null,`Invalid semantic version comparator: ${$}`);let K=a8(X,Q[2]);switch(Q[1]??"="){case">=":return K>=0;case"<=":return K<=0;case">":return K>0;case"<":return K<0;default:return K===0}}function W$(X,$=U5){return $.split(/\s+/).filter(Boolean).every((Q)=>t8(X,Q))}function H$(X){return`ECV(meta.run) ecv=${Q1} catalog=${X}`}function J$(X,$,Q){z(W$(X.sdk.version),`Runtime SDK ${X.sdk.version} is not compatible with ECV ${Q1} (${U5})`),z(!G$.includes(X.sdk.version),`Runtime SDK ${X.sdk.version} is not accepted by ECV ${Q1}; upgrade the SDK`),Q?.annotate?.(X.sdk.version,"ecv.sdk"),Q?.annotate?.(Q1,"ecv.version"),Q?.annotate?.(String(X.sdk.catalogVersion),"ecv.catalog"),Q?.annotate?.([...$].sort().join(","),"ecv.claims")}function j$(X,$,Q){let K=new Set($),Y=$.filter((G)=>X.capabilities[G]?.testable!==!0);z(Y.length===0,`Claimed capabilities are not testable: ${Y.map((G)=>`${G} (${X.capabilities[G]?.reason??"no manifest state"})`).join("; ")}`);let Z=Object.keys(X.capabilities).filter((G)=>X.capabilities[G].testable&&!K.has(G));if(Z.length>0)Q?.annotate?.(`Runtime capabilities not claimed by this suite: ${Z.join(",")}`,"ecv.unclaimed")}import{createServer as r8}from"node:http";import{ionite as u1}from"@ionite/server";import{createValidators as l1}from"@ionite/valibot";var b5=(X)=>new Promise(($)=>setTimeout($,X));async function D1(X,$=1500,Q=20){let K=Date.now()+$;while(Date.now()<K){if(X())return!0;await b5(Q)}return X()}async function s8(){let X=[],$=r8((K,Y)=>{let Z=[];K.on("data",(G)=>Z.push(G)),K.on("end",()=>{X.push({path:K.url??"",headers:K.headers,body:Buffer.concat(Z)}),Y.statusCode=200,Y.end()})});await new Promise((K,Y)=>{$.once("error",Y),$.listen(0,"127.0.0.1",K)});let Q=$.address();if(!Q||typeof Q==="string")throw Error("OTLP recorder did not bind a TCP port");return{endpoint:`http://127.0.0.1:${Q.port}`,requests:X,close:()=>new Promise((K,Y)=>$.close((Z)=>Z?Y(Z):K()))}}function N2(X){let $=X,Q;return{async load(){return $},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){$=K,Q?.(K)},secret:{}}}function e8(X){let Q=X,K=1,Y=new Set,Z=()=>({data:structuredClone(Q),metadata:{path:"/kms/otel-ecv",version:K,created:new Date(0)}});return{secret:{type:"dev",isReady:()=>!0,ready:async()=>{},get:()=>{throw Error("not implemented")},getFullSecret:async()=>Z(),getSecret:async()=>structuredClone(Q),putSecret:async()=>{},deleteSecret:async()=>{},listPaths:async()=>[],exists:async()=>!0,requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async()=>{throw Error("parkKmsKey is not implemented for this test source")},addKmsKeys:async()=>{throw Error("addKmsKeys is not implemented for this test source")},revokeKmsKey:async()=>{throw Error("revokeKmsKey is not implemented for this test source")},getMetadata:async()=>({}),subscribe:async(W,H)=>{return Y.add(H),H(Z()),()=>Y.delete(H)}},async load(){return{id:"otel-kms",kms:{"otel-ecv":{path:"/kms/otel-ecv"}},otel:{...y5,apiKeyPool:"otel-ecv"}}},async subscribe(){return()=>{}},pushPool(W){Q=W,K+=1;for(let H of Y)H(Z())}}}var y5={name:"ecv-otel",traces:!1,metrics:!1,logs:!1};function $Q(){return{name:"OTLP logs use HTTP/protobuf by default and preserve configured exporter headers",fn:async()=>{let X=await s8(),$=N2({id:"otel-protobuf-wire",otel:{name:"ecv-otel-wire",endpoint:X.endpoint,headers:{"x-ecv-api-key":"wire-secret"},traces:!1,metrics:!1,logs:!0}}),Q=u1($,{validators:l1(),otel:{console:!1}});try{await Q.ready(1000),z(await D1(()=>Q.otel?.isReady?.()===!0),"Expected OTel ready before emitting a log"),Q.otel?.logger.info("ecv protobuf wire marker",{"ecv.marker":"protobuf"}),await Q.otel?.forceFlush?.(),z(await D1(()=>X.requests.length>0),"Expected the OTLP collector to receive a log");let K=X.requests[0];z(K?.path==="/v1/logs",`Expected OTLP logs path, got ${K?.path??"no request"}`);let Y=String(K?.headers["content-type"]??"");z(Y.includes("application/x-protobuf"),`Expected protobuf content type, got ${Y||"none"}`),z(K?.headers["x-ecv-api-key"]==="wire-secret","Expected configured exporter header"),z((K?.body.byteLength??0)>0,"Expected a non-empty OTLP protobuf body")}finally{await Q.otel?.shutdown?.(),Q.close(),await X.close()}}}}function O2(){return{tests:[{name:"otel module is inactive (not-ready) when RemoteConfig omits otel (§22.2)",fn:async()=>{let $=N2({id:"otel-off"}),Q=u1($,{validators:l1()});try{await Q.ready(500),z(Q.otel?.isReady?.()!==!0,"Expected ion.otel to be not-ready with no RemoteConfig.otel");let K=Q.readiness();w(K.modules.otel.active,!1,"Expected modules.otel.active === false when unconfigured");let Y=Q.requireReady(["otel"]);z(Y.ok===!1,'Expected requireReady(["otel"]) to fail when otel is unconfigured'),z(Y.notReady.some((Z)=>Z.name==="otel"),"Expected otel to be listed among the not-ready dependencies")}finally{Q.close?.()}}},$Q(),{name:"RemoteConfig.otel activates the module; de-configuration re-arms readiness (§22.2)",fn:async()=>{let $=N2({id:"otel-rearm",otel:{...y5}}),Q=u1($,{validators:l1()});try{await Q.ready(1000),z(Q.otel?.isReady?.()===!0,"Expected ion.otel.isReady() true after RemoteConfig.otel applied");let K=Q.readiness();w(K.modules.otel.active,!0,"Expected modules.otel.active === true once configured"),w(K.modules.otel.ready,!0,"Expected modules.otel.ready === true once started"),z(Q.requireReady(["otel"]).ok===!0,'Expected requireReady(["otel"]) to pass when active+ready'),$.push({id:"otel-rearm"}),await D1(()=>Q.otel?.isReady?.()!==!0),z(Q.otel?.isReady?.()!==!0,"Expected ion.otel.isReady() false after de-configuration");let Y=Q.readiness();w(Y.modules.otel.active,!1,"Expected modules.otel.active === false after de-config"),z(Q.requireReady(["otel"]).ok===!1,'Expected requireReady(["otel"]) to fail once otel is de-configured');let Z=!1;try{await Q.otel?.ready?.(50)}catch{Z=!0}z(Z,"Expected a re-armed ion.otel.ready(50) to reject while de-configured");let G={resolved:!1},W=Q.otel?.ready?.().then(()=>{G.resolved=!0}).catch(()=>{});await b5(60);let H=G.resolved===!1;z(H,"Expected a fresh no-timeout ready() to stay pending while inactive"),$.push({id:"otel-rearm",otel:{...y5}}),await D1(()=>Q.otel?.isReady?.()===!0),await W;let J=G.resolved===!0;z(J,"Expected the re-armed ready() waiter to resolve after re-activation"),z(Q.otel?.isReady?.()===!0,"Expected ion.otel.isReady() true after re-activation"),z(Q.requireReady(["otel"]).ok===!0,'Expected requireReady(["otel"]) to pass again after re-activation')}finally{Q.close?.()}}},{name:"otel rebuilds exporters when its referenced KMS key rotates",fn:async()=>{let $=e8({kind:"kms",slots:["k1"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()}]}),Q=u1($,{validators:l1()});try{await Q.ready(1000),z(await D1(()=>Q.otel?.isReady?.()===!0),"Expected OTel to start with KMS token");let K=0,Y=Q.otel?.afterChange?.(()=>{K+=1});$.pushPool({kind:"kms",slots:["k2"],keys:[{id:"k1",state:"deprecated",value:"token-1",notAfter:new Date(Date.now()+60000).toISOString()},{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),z(await D1(()=>K>0),"Expected KMS active-key change to rebuild managed OTel"),z(await D1(()=>Q.otel?.isReady?.()===!0),"Expected OTel ready after credential rebuild"),w(K,1,"Expected the active-key rotation to rebuild managed OTel exactly once");let Z=Q.kms?.getPool("otel-ecv");z(Z?.accepts("token-1")===!0,"Expected OTel KMS overlap to accept the deprecated key"),z(Z.accepts("token-2"),"Expected OTel KMS overlap to accept the new active key"),w(await Z.value(),"token-2","Expected OTel KMS value() to expose the rotated active key"),$.pushPool({kind:"kms",slots:["k2"],keys:[{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),z(!Z.accepts("token-1"),"Expected overlap removal to stop accepting the deprecated key"),await b5(80),w(K,1,"Expected overlap-only cleanup not to rebuild OTel when the active key is unchanged"),z(Q.otel?.isReady?.()===!0,"Expected OTel to remain ready after overlap-only cleanup"),Y?.()}finally{Q.close()}}},{name:"an invalid RemoteConfig.otel provider config fails the config apply (§22.2)",fn:async()=>{let $=N2({id:"otel-invalid",otel:{type:"datadog",name:"ecv-otel",traces:!1,metrics:!1,logs:!1}}),Q=u1($,{validators:l1()});try{let K=!1;try{await Q.ready(500)}catch{K=!0}z(K,"Expected an invalid otel provider config to reject readiness at apply"),z(Q.configStatus?.lastError!==void 0,"Expected configStatus.lastError to record the failed otel apply")}finally{Q.close?.()}}}]}}async function QQ(){let X=performance.now(),{tests:$}=O2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"OpenTelemetry",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{ionite as K1}from"@ionite/server";import{createValidators as Y1}from"@ionite/valibot";var Z1="/api/ionite",X1="http://localhost";function B1(X){return{async load(){return X},async subscribe(){return},secret:{}}}async function _1(X){let $=await X.text();try{return JSON.parse($)}catch{throw Error(`Expected a JSON body, got: ${$.slice(0,120)}`)}}function M2(){return{tests:[{name:"POST {basePath}/readyz returns a structured 405 with an Allow header (§11.2)",fn:async()=>{let X=K1(B1({id:"protocol-readyz-405"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/readyz`,{method:"POST"}));w($.status,405,"POST to readyz must be 405 Method Not Allowed"),z($.headers.get("Allow")?.includes("GET")??!1,"Allow header must advertise GET");let Q=await _1($);w(Q.error,"method_not_allowed","body carries the stable method_not_allowed code"),X.close?.()}},{name:"GET {basePath}/readyz is reachable and reports config applied",fn:async()=>{let X=K1(B1({id:"protocol-readyz-get"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/readyz`));z($.status===200||$.status===503,"readyz GET returns a readiness status");let Q=await _1($);w(Q.applied,!0,"readyz reports config applied after ion.ready()"),X.close?.()}},{name:"POST {basePath}/capabilitiez returns 405 before authentication (§11.2)",fn:async()=>{let X=K1(B1({id:"protocol-cap-405",capabilitiez:!0}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/capabilitiez`,{method:"POST"}));w($.status,405,"POST to capabilitiez must be 405");let Q=await _1($);w(Q.error,"method_not_allowed","capabilitiez 405 carries the stable code"),X.close?.()}},{name:"GET {basePath}/capabilitiez returns JSON 404 when disabled (§11.2)",fn:async()=>{let X=K1(B1({id:"protocol-cap-disabled"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/capabilitiez`));w($.status,404,"disabled capabilitiez is 404");let Q=await _1($);w(Q.error,"not_found","disabled capabilitiez returns the stable JSON not_found code"),X.close?.()}},{name:"GET {basePath}/capabilitiez returns 503 before config is applied",fn:async()=>{let X,Q=K1({load:()=>new Promise((Z)=>{X=Z}),async subscribe(){return},secret:{}},{validators:Y1()}),K=await Q.handler(new Request(`${X1}${Z1}/capabilitiez`));w(K.status,503,"capabilitiez must not expose a manifest before config is applied");let Y=await _1(K);w(Y.error,"ionite is not ready","not-ready capabilitiez returns the stable error code"),X?.({id:"protocol-cap-not-ready",capabilitiez:!0}),await Q.ready(1000),Q.close?.()}},{name:"GET {basePath}/capabilitiez requires workload authentication when enabled",fn:async()=>{let X=K1(B1({id:"protocol-cap-auth",capabilitiez:!0}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/capabilitiez`));w($.status,401,"enabled capabilitiez must reject an unauthenticated caller"),X.close?.()}},{name:"an unknown ionite route returns JSON 404 { error: not_found } (§9.2)",fn:async()=>{let X=K1(B1({id:"protocol-unknown-404"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/does-not-exist`));w($.status,404,"unknown route is 404");let Q=await _1($);w(Q.error,"not_found","unknown route returns the stable JSON not_found code"),X.close?.()}},{name:"a throwing handler is caught and sanitized to 500 { error: internal_error } (§9.2)",fn:async()=>{let $=K1(B1({id:"protocol-throw-500"}),{validators:Y1(),routing:{resolve:()=>({module:"sso",kind:"sso.handler",handler:async()=>{throw Error("LEAKED_INTERNAL_DETAIL_42")}})}});await $.ready(1000);let Q=await $.handler(new Request(`${X1}${Z1}/anything`));w(Q.status,500,"an unhandled handler throw becomes a 500");let K=await Q.text();z(!K.includes("LEAKED_INTERNAL_DETAIL_42"),"the raw error message must not leak to the caller");let Y=JSON.parse(K);w(Y.error,"internal_error","a thrown error collapses to the stable internal_error code"),$.close?.()}}]}}async function KQ(){let{tests:X}=M2(),$=[];for(let Q of X)try{await Q.fn(),$.push({name:Q.name,passed:!0})}catch(K){$.push({name:Q.name,passed:!1,error:K instanceof Error?K.message:String(K)})}return $}import{defineSecretType as YQ,SecretContentTypeMismatchError as ZQ,waitOn as XQ}from"@ionite/core";import{generateLfvEd25519KeyPair as E5,signLfvPayload as P$,stringifyLfvJwks as h5}from"@ionite/core/server";import{lfvVault as C5,subscribeSecretPathWithHttpPolling as zQ,vault as w$}from"@ionite/server";var I0={lfvServerUrl:"http://localhost:3599/api/lfv",clientId:"ecv-hardening-client",path:"/ecv/hardening/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"},o0=(X)=>new Promise(($)=>setTimeout($,X));async function D$(X,$,Q,K){let Y=String($.request_id),Z="issued_at"in $?$:{...$,issued_at:Date.now()},G=JSON.stringify(Z),W=await P$({privateKeyJwk:Q,requestId:Y,rawBody:G});return new Request(`http://localhost${X}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":Y,"X-LFV-Key-Id":K,"X-LFV-Signature":W},body:G})}async function GQ(X,$,Q){let K=String(X.request_id),Y=JSON.stringify(X),Z=await P$({privateKeyJwk:$,requestId:K,rawBody:Y});return new Response(Y,{status:200,headers:{"Content-Type":"application/json","X-LFV-Request-Id":K,"X-LFV-Key-Id":Q,"X-LFV-Signature":Z}})}function B$(X){if(typeof X==="string")return X;if(X instanceof URL)return X.toString();return X.url}function WQ(X,$=5){let Q=[],K=X;for(let Y=0;Y<$&&K!=null;Y+=1)if(K instanceof Error)Q.push(K.message),K=K.cause;else{Q.push(String(K));break}return Q.join(" <- ")}function HQ(X,$){let Q=X?.headers;if(!Q)return null;if(Q instanceof Headers)return Q.get($);if(Array.isArray(Q)){let Y=Q.find(([Z])=>Z.toLowerCase()===$.toLowerCase());return Y?Y[1]:null}let K=Q;for(let[Y,Z]of Object.entries(K))if(Y.toLowerCase()===$.toLowerCase())return Z;return null}function T2(){return{tests:[{name:"upstream error bodies are redacted from thrown errors (no secret leaks into configStatus.lastError)",fn:async()=>{let Q=w$({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),K=globalThis.fetch;globalThis.fetch=async()=>{return new Response(JSON.stringify({errors:["SECRET-DO-NOT-LEAK-7f3a91"],leaked:"SECRET-DO-NOT-LEAK-7f3a91"}),{status:500,statusText:"Internal Server Error"})};try{let Y=!1,Z="";try{await Q.getSecret("app/config")}catch(G){Y=!0,Z=WQ(G)}z(Y,"a non-200 vault read must throw (fail closed), not return stale/empty data"),z(!Z.includes("SECRET-DO-NOT-LEAK-7f3a91"),`thrown error must not include the upstream body (got: ${Z})`),z(Z.includes("500"),`redacted error should still carry the structured status (got: ${Z})`)}finally{globalThis.fetch=K,Q.close?.()}}},{name:"http polling never overlaps and surfaces degraded/recovered health",fn:async()=>{let $=0,Q=0,K=0,Y=!1,Z=[],W=await zQ({getFullSecret:async(J)=>{if($+=1,Q=Math.max(Q,$),K+=1,await o0(25),$-=1,Y)throw Error("simulated upstream failure");return{data:{},metadata:{path:J,version:1,created:new Date}}}},"/p",()=>{},15,void 0,void 0,{onHealth:(J)=>Z.push(J)});await o0(180),w(Q,1,"HTTP polls must never overlap (self-scheduling chain)"),Y=!0,await o0(120),z(Z.includes(!1),"a failed poll must surface unhealthy (degraded isAvailable) instead of hammering"),Y=!1,await o0(120),w(Z[Z.length-1],!0,"recovery must surface healthy again"),W();let H=K;await o0(120),w(K,H,"no polls may run after unsubscribe (loop halts on teardown)")}},{name:"secret handles lazily decode typed env, text, binary, and legacy JSONC values",fn:async()=>{let $=w$({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),Q=globalThis.fetch;globalThis.fetch=async(K)=>{let Y=new URL(B$(K)).pathname.split("/").pop(),Z={path:`/${Y}`,version:1,created_time:new Date(0).toISOString()};if(Y==="env")return Response.json({data:{data:{API_KEY:"secret",PORT:"443"},metadata:{...Z,content_type:{format:"env",mime_type:"text/x-dotenv"}}}});if(Y==="text")return Response.json({data:{data:{content:"hello"},metadata:{...Z,content_type:{format:"text",mime_type:"text/plain"}}}});if(Y==="binary")return Response.json({data:{data:{content_base64:"AAEC/w=="},metadata:{...Z,content_type:{format:"binary",mime_type:"application/octet-stream"}}}});return Response.json({data:{data:{legacy:!0},metadata:Z}})};try{let K=await $.get("env").env();w(K.API_KEY,"secret","env() must decode the flat key/value object"),w(K.PORT,"443","env() must preserve string values"),w(await $.get("text").text(),"hello","text() must unwrap the content field");let Y=await $.get("binary").bytes();w(Array.from(Y).join(","),"0,1,2,255","bytes() must fully decode base64");let Z=await $.get("legacy").json();w(Z.legacy,!0,"legacy secrets without content_type must default to JSONC");let G=YQ({id:"expected-json",format:"jsonc"}),W;try{await $.get("text").parse(G)}catch(J){W=J}z(W instanceof ZQ,"parse() must throw the structured mismatch error before decoding");let H=W;w(H.expected.mimeType,"application/json","mismatch must expose expected MIME type"),w(H.received.mimeType,"text/plain","mismatch must expose received MIME type")}finally{globalThis.fetch=Q,$.close?.()}}},{name:"LFV OTP-as-possession-proof: no client secret, OTP arrives only via signed callback",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await E5("ecv-otp-kid"),K=C5({...I0,verifyJwks:h5(Q)}),Y=0,Z=0,G=!1,W=7,H=globalThis.fetch;globalThis.fetch=async(J,j)=>{let D=B$(J);if(HQ(j,"X-LFV-Signature")!==null)G=!0;let P=JSON.parse(String(j?.body??"{}"));if(D.endsWith("/otp"))return Y+=1,await K.handleDelivery(await D$(I0.deliveryEndpoint,{request_id:P.request_id,type:"otp",otp:"otp-value",path:I0.path,action:P.action},$,"ecv-otp-kid")),new Response(JSON.stringify({request_id:P.request_id,status:"pending"}),{status:202});if(D.endsWith("/secrets/request"))return Z+=1,GQ({request_id:P.request_id,status:"completed",path:I0.path,data:{data:{marker:"possession-ok"},metadata:{path:I0.path,version:W,content_type:{format:"jsonc",mime_type:"application/vnd.ionite.ecv+json",schema_url:"https://example.com/ecv.schema.json",type_id:"ecv-payload"}}}},$,"ecv-otp-kid");return new Response("{}",{status:200})};try{let J=await Promise.all(Array.from({length:5},()=>K.getSecret(I0.path)));for(let D of J)w(D.marker,"possession-ok","each concurrent read should resolve from the one cycle");let j=await K.getFullSecret(I0.path);w(j.metadata.contentType?.format,"jsonc","LFV delivery must preserve the secret format"),w(j.metadata.contentType?.mimeType,"application/vnd.ionite.ecv+json","LFV delivery must preserve the MIME type"),w(j.metadata.contentType?.schemaUrl,"https://example.com/ecv.schema.json","LFV delivery must preserve the schema URL"),w(j.metadata.contentType?.typeId,"ecv-payload","LFV delivery must preserve the type id"),w(Y,1,"concurrent reads must issue exactly one OTP request (single-flight)"),w(Z,1,"concurrent reads must issue exactly one secret request (single-flight)"),z(!G,"client must not send X-LFV-Signature (no client-held secret)")}finally{globalThis.fetch=H,K.close?.()}}},{name:"self-redeeming LFV event storms single-flight OTP issuance per path",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await E5("ecv-event-flight-kid"),K=C5({...I0,verifyJwks:h5(Q),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),Y=globalThis.fetch,Z=0,G,W=new Promise((H)=>{G=H});globalThis.fetch=async()=>{return Z+=1,W};try{for(let H=0;H<5;H+=1){let J=await K.handleEvents(await D$(I0.eventsEndpoint,{request_id:crypto.randomUUID(),path:I0.path,event:"updated"},$,"ecv-event-flight-kid"));w(J.status,200,"event callback is accepted")}await o0(20),w(Z,1,"concurrent event refreshes issue one OTP request for the path"),G?.(Response.json({status:"pending"},{status:202})),await o0(20)}finally{G?.(Response.json({status:"pending"},{status:202})),globalThis.fetch=Y,K.close?.()}}},{name:"LFV close() mid-outage settles pending waiters instead of leaking them",fn:async()=>{let{publicJwks:$}=await E5("ecv-close-kid"),Q=C5({...I0,verifyJwks:h5($)}),K=globalThis.fetch;globalThis.fetch=async(Y,Z)=>{let G=JSON.parse(String(Z?.body??"{}"));return new Response(JSON.stringify({request_id:G.request_id,status:"pending"}),{status:202})};try{let Y=Q.getSecret(I0.path),Z=!1;Y.catch(()=>{Z=!0}),await o0(40),Q.close?.();let G=!1,W="";try{await Y}catch(H){G=!0,W=H instanceof Error?H.message:String(H)}z(G,"close() must settle the pending read (reject), not leak the waiter"),z(W.toLowerCase().includes("closed"),`rejection should explain the source closed (got: ${W})`),z(Z,"pending waiter promise must settle after close()")}finally{globalThis.fetch=K}}},{name:"waitOn probes never overlap and stop after success (timers cleared)",fn:async()=>{let $=0,Q=0,K=0,Y=!1,Z=globalThis.fetch;globalThis.fetch=async()=>{return $+=1,Q+=1,K=Math.max(K,Q),await o0(40),Q-=1,new Response("{}",{status:200})};let G=setTimeout(()=>{Y=!0},150);try{await XQ("http://localhost/ecv/health",()=>Y,10,0,5000),w(K,1,"waitOn must never run overlapping probes (self-scheduling chain)"),z($>=2,"the probe chain should have iterated at least twice before becoming ready");let W=$;await o0(120),w($,W,"no probe may fire after waitOn resolves (retry timer cleared)")}finally{clearTimeout(G),globalThis.fetch=Z}}}]}}async function JQ(){let X=performance.now(),{tests:$}=T2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Secrets hardening",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}var L$={loginPath:"/api/auth/login",callbackPath:"/api/auth/callback",userPath:"/api/auth/user",logoutPath:"/api/auth/logout",backChannelLogoutPath:"/api/auth/logout/backchannel",tokenPath:"/api/auth/token",refreshPath:"/api/auth/refresh"};async function V$(X,$){return N("SSO Login Endpoint",async()=>{let Q=X.loginPath??"/api/auth/login",K=await $(Q);z(K.status===302,`Expected 302 redirect, got ${K.status}`);let Y=K.headers.get("Location");if(z(Y!==null,"Missing Location header in redirect"),X.expectedAuthorizationUrlPattern)z(X.expectedAuthorizationUrlPattern.test(Y),`Authorization URL does not match expected pattern: ${Y}`);let Z=new URL(Y);z(Z.searchParams.has("client_id"),"Missing client_id in authorization URL"),z(Z.searchParams.has("redirect_uri"),"Missing redirect_uri in authorization URL"),z(Z.searchParams.has("response_type"),"Missing response_type in authorization URL"),z(Z.searchParams.has("scope"),"Missing scope in authorization URL"),z(Z.searchParams.has("state"),"Missing state in authorization URL"),z(Z.searchParams.has("code_challenge"),"Missing code_challenge (PKCE) in authorization URL"),z(Z.searchParams.has("code_challenge_method"),"Missing code_challenge_method (PKCE) in authorization URL");let G=y0(K.headers),W=Array.from(G.keys()).some((H)=>H.includes(".state"));return z(W,"Missing state cookie in response"),{details:{authorizationUrl:Y,pkceEnabled:!0,stateParameter:Z.searchParams.get("state")}}})}async function N$(X,$){return N("SSO User Endpoint (Unauthenticated)",async()=>{let Q=X.userPath??"/api/auth/user",K=await $(Q);return z(K.status===401,`Expected 401 Unauthorized, got ${K.status}`),{details:{status:K.status}}})}async function O$(X,$){return N("SSO Logout Endpoint",async()=>{let Q=X.logoutPath??"/api/auth/logout",K=await $(`${Q}?redirect=/`);z(K.status===302||K.status===200,`Expected 302 or 200, got ${K.status}`);let Y=y0(K.headers),Z=[];for(let[H,J]of Y.entries())if(J===""||J.includes("Max-Age=0"))Z.push(H);let G=K.headers.getSetCookie?.()??[],W=G.some((H)=>H.includes("Max-Age=0")||H.includes("expires=Thu, 01 Jan 1970"));return z(W||Z.length>0||G.length===0,"Logout should clear session cookies"),{details:{status:K.status,clearedCookies:Z}}})}async function M$(X,$){return N("SSO Back-Channel Logout Endpoint",async()=>{let Q=X.backChannelLogoutPath??"/api/auth/logout/backchannel",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:""});return z(K.status===400||K.status===200||K.status===404,`Expected 400 or 200, got ${K.status}`),{details:{status:K.status,endpointExists:K.status!==404}}})}async function T$(X,$){return N("SSO Callback Endpoint (Invalid)",async()=>{let Q=X.callbackPath??"/api/auth/callback",K=await $(`${Q}?code=invalid&state=invalid`);z(K.status===302,`Expected redirect for invalid callback, got ${K.status}`);let Y=K.headers.get("Location")??"";if(!Y.startsWith("/")){let Z=new URL(`${T0(X)}/`).origin;z(new URL(Y).origin===Z,`Failure redirect must stay same-origin: ${Y}`)}return{details:{status:K.status,location:Y,handlesInvalidCallback:!0}}})}async function jQ(X,$,Q){return N("SSO Callback Error Surfaced",async()=>{let K=X.callbackPath??"/api/auth/callback",Y=await $(`${K}?code=invalid&state=invalid`);z(Y.status===302,`Expected redirect for invalid callback, got ${Y.status}`);let Z=Y.headers.get("Location")??"";return z(Q.test(Z),`Failure redirect does not surface an error code (expected ${Q}): ${Z}`),{details:{status:Y.status,location:Z}}})}async function x$(X,$){if(!X.tokenPath)return W1("SSO Token Endpoint (Unauthenticated)","Token path not configured");let Q=X.tokenPath;return N("SSO Token Endpoint (Unauthenticated)",async()=>{let K=await $(Q);return z(K.status===401||K.status===404,`Expected 401 Unauthorized or 404, got ${K.status}`),{details:{status:K.status}}})}async function _$(X,$){if(!X.refreshPath)return W1("SSO Refresh Endpoint (Unauthenticated)","Refresh path not configured");let Q=X.refreshPath;return N("SSO Refresh Endpoint (Unauthenticated)",async()=>{let K=await $(Q);return z(K.status===401||K.status===404,`Expected 401 Unauthorized or 404, got ${K.status}`),{details:{status:K.status}}})}function q$(X){return Array.from(X.keys()).filter(($)=>$.includes(".state"))}function F$(X){return X.endsWith(".access")||X.endsWith(".id")||X.endsWith(".refresh")||X.endsWith(".control")}function wQ(X){for(let $ of X.keys()){let Q=$.indexOf(".state");if(Q>0)return $.slice(0,Q);if($.endsWith(".session"))return $.slice(0,-8)}return}function DQ(X,$){return(X.getSetCookie?.()??[]).some((K)=>{let Y=K.split("=")[0]?.trim()??"";if(!$(Y))return!1;return K.includes("Max-Age=0")||/expires=Thu, 01 Jan 1970/i.test(K)})}async function A5(X,$,Q={}){let K=X.loginPath;if(!K)throw Error("Login path not configured");let Y=Q.redirectParam?`?redirect=${encodeURIComponent(Q.redirectParam)}`:"",Z=await $(`${K}${Y}`);if(Z.status!==302)throw Error(`Login did not redirect: ${Z.status}`);let G=Z.headers.get("Location");if(!G)throw Error("No redirect location from login");let W=y0(Z.headers),H=new URL(G).searchParams.get("state");if(Q.bogusCode){let B=new URL(G).searchParams.get("redirect_uri");if(!B||!H)throw Error("Missing redirect_uri/state on authorization URL");let q=new URL(B).pathname;return{callback:await $(`${q}?code=bogus-code&state=${encodeURIComponent(H)}`,{headers:{Cookie:f0(W)}}),state:H}}let J=await globalThis.fetch(G,{redirect:"manual"});if(J.status!==302)throw Error(`Auth did not redirect: ${J.status}`);let j=J.headers.get("Location");if(!j)throw Error("No redirect location from auth");let D=new URL(j).pathname+new URL(j).search;return{callback:await $(D,{headers:{Cookie:f0(W)}}),state:H}}async function R$(X,$){return N("SSO State Cookie Multi-Tab Isolation",async()=>{let Q=X.loginPath??"/api/auth/login",[K,Y]=await Promise.all([$(Q),$(Q)]);z(K.status===302&&Y.status===302,"Both login attempts should redirect");let Z=new URL(K.headers.get("Location")??"").searchParams.get("state"),G=new URL(Y.headers.get("Location")??"").searchParams.get("state");z(!!Z&&!!G&&Z!==G,"Each login attempt must mint a distinct state");let W=q$(y0(K.headers)),H=q$(y0(Y.headers));z(W.length===1&&H.length===1,"Each login should set exactly one state cookie"),z(W[0]!==H[0],"Concurrent logins must use isolated (distinct-name) state cookies"),z(W[0].includes(Z)&&H[0].includes(G),"The state value must be encoded in the state cookie name");let J=(K.headers.getSetCookie?.()??[]).find((j)=>j.split("=")[0]?.includes(".state"));return z(!!J,"Login must emit a Set-Cookie for the state cookie"),z(/SameSite=Lax/i.test(J),`State cookie must be SameSite=Lax for the cross-site IdP callback, got: ${J}`),{details:{state1:Z,state2:G,cookie1:W[0],cookie2:H[0],stateSetCookie:J}}})}async function v$(X,$){return N("SSO State Cookie Cleared On Error",async()=>{let Q;try{Q=await A5(X,$,{bogusCode:!0})}catch(Z){return{details:{skipped:!0,reason:Z instanceof Error?Z.message:String(Z)}}}let{callback:K,state:Y}=Q;return z(K.status===302,`Failed callback should redirect, got ${K.status}`),z(DQ(K.headers,(Z)=>Z.includes(".state")&&(Y?Z.includes(Y):!0)),"Callback error path must clear the per-attempt state cookie"),{details:{status:K.status,state:Y}}})}async function U$(X,$){return N("SSO Open-Redirect Denied (same-origin floor)",async()=>{let Q;try{Q=await A5(X,$,{redirectParam:"https://evil.example/pwned"})}catch(Z){return{details:{skipped:!0,reason:Z instanceof Error?Z.message:String(Z)}}}let{callback:K}=Q;z(K.status===302,`Successful callback should redirect, got ${K.status}`);let Y=K.headers.get("Location")??"";if(z(!Y.includes("evil.example"),`Open redirect leaked to external origin: ${Y}`),!Y.startsWith("/")){let Z=new URL(`${T0(X)}/`).origin;z(new URL(Y).origin===Z,`Post-login redirect must stay same-origin: ${Y}`)}return{details:{location:Y}}})}async function b$(X,$){return N("SSO Opaque Session Cookie Only",async()=>{let{cookies:Q,success:K,error:Y}=await d1(X,$);z(K,Y||"Login flow failed");let Z=Array.from(Q.keys());z(Z.some((j)=>j.includes(".session")),"Opaque login must set a `.session` cookie");let G=Z.find(F$);z(!G,`Opaque-only login must not set token cookies, found: ${G}`);let W=X.userPath;if(!W)throw Error("User path not configured");let H=await $(W,{headers:{Cookie:f0(Q)}});z(H.status===200,`User endpoint returned ${H.status} after login, expected 200`);let J=Array.from(y0(H.headers).keys()).find(F$);return z(!J,`Session resolution must not set token cookies, found: ${J}`),{details:{sessionCookieOnly:!0}}})}async function y$(X,$){return N("SSO Legacy Token Cookies Rejected",async()=>{let Q=X.loginPath??"/api/auth/login",K=X.userPath??"/api/auth/user",Y=await $(Q);z(Y.status===302,`Login should redirect, got ${Y.status}`);let Z=wQ(y0(Y.headers));if(!Z)return{details:{skipped:!0,reason:"Could not derive cookie prefix from login response"}};let G=new Map([[`${Z}.access`,"legacy-access"],[`${Z}.id`,"legacy-id"],[`${Z}.refresh`,"legacy-refresh"],[`${Z}.control`,btoa(JSON.stringify({token_type:"Bearer",expires_in:3600}))]]),W=await $(K,{headers:{Cookie:f0(G)}});return z(W.status===401,`Legacy token cookies must not authenticate (expected 401), got ${W.status}`),{details:{status:W.status,prefix:Z}}})}async function d1(X,$){let Q=X.loginPath;if(!Q)return{cookies:new Map,success:!1,error:"Login path not configured"};try{let K=await $(Q);if(K.status!==302)return{cookies:new Map,success:!1,error:`Login did not redirect: ${K.status}`};let Y=K.headers.get("Location");if(!Y)return{cookies:new Map,success:!1,error:"No redirect location from login"};let Z=y0(K.headers),G=await globalThis.fetch(Y,{redirect:"manual"});if(G.status!==302)return{cookies:new Map,success:!1,error:`Auth did not redirect: ${G.status}`};let W=G.headers.get("Location");if(!W)return{cookies:new Map,success:!1,error:"No redirect location from auth"};let H=new URL(W).pathname+new URL(W).search,J=await $(H,{headers:{Cookie:f0(Z)}});if(J.status!==302)return{cookies:new Map,success:!1,error:`Callback did not redirect: ${J.status}`};return{cookies:y0(J.headers),success:!0}}catch(K){return{cookies:new Map,success:!1,error:K instanceof Error?K.message:String(K)}}}async function E$(X,$){return N("SSO JIT User Provisioning Flow",async()=>{let{cookies:Q,success:K,error:Y}=await d1(X,$);z(K,Y||"Login flow failed");let Z=Array.from(Q.keys()).some((J)=>J.includes(".session"));z(Z,"No opaque session cookie set after login");let G=X.userPath;if(!G)throw Error("User path not configured");let W=await $(G,{headers:{Cookie:f0(Q)}});z(W.status===200,`User endpoint returned ${W.status} after login, expected 200`);let H=await W.json();return z(H.id!==void 0,"User data missing id field"),BQ(X,H),{details:{userId:H.id,userName:H.userName,name:H.name,email:H.email,avatar:H.avatar,jitFlowCompleted:!0}}})}function BQ(X,$){let Q=X.expectedUser;if(!Q)return;if(Q.id!==void 0)z($.id===Q.id,`Expected user id ${Q.id}, got ${String($.id)}`);if(Q.userName!==void 0)z($.userName===Q.userName,`Expected username ${Q.userName}, got ${String($.userName)}`);if(Q.name!==void 0)z($.name===Q.name,`Expected name ${Q.name}, got ${String($.name)}`);if(Q.email!==void 0)z($.email===Q.email,`Expected email ${Q.email}, got ${String($.email)}`);if(Q.avatar!==void 0)z($.avatar===Q.avatar,`Expected avatar ${Q.avatar}, got ${String($.avatar)}`);if(Q.requireSso)z($.sso!==void 0,"Expected SSO auth envelope to be preserved on user response")}async function PQ(X){let $=performance.now(),Q={...L$,...X},K=r(Q),Y=[];Y.push(await V$(Q,K)),Y.push(await N$(Q,K)),Y.push(await O$(Q,K)),Y.push(await M$(Q,K)),Y.push(await T$(Q,K)),Y.push(await x$(Q,K)),Y.push(await _$(Q,K)),Y.push(await R$(Q,K)),Y.push(await v$(Q,K)),Y.push(await U$(Q,K)),Y.push(await y$(Q,K)),Y.push(await E$(Q,K)),Y.push(await b$(Q,K));let Z=performance.now()-$,G=Y.filter((J)=>J.passed).length,W=Y.filter((J)=>!J.passed).length,H=Y.filter((J)=>J.details?.skipped).length;return{suite:"SSO",passed:W===0,tests:Y,duration:Z,summary:{total:Y.length,passed:G-H,failed:W,skipped:H}}}function x2(X){let $={...L$,...X},Q=r($);return{tests:[{name:"login endpoint redirects to authorization URL",fn:async()=>{let Z=await V$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"user endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await N$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"callback endpoint redirects on invalid requests",fn:async()=>{let Z=await T$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"token endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await x$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"refresh endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await _$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"state cookie is isolated per login attempt (multi-tab)",fn:async()=>{let Z=await R$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"state cookie is cleared on callback error",fn:async()=>{let Z=await v$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"cross-origin post-login redirect is floored to same-origin",fn:async()=>{let Z=await U$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"legacy token cookies do not authenticate (opaque-only)",fn:async()=>{let Z=await y$($,Q);if(!Z.passed)throw Error(Z.error)}}],ext:{createJITTests:()=>[{name:"user provisioning flow completes successfully",fn:async()=>{let Z=await E$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"login sets only an opaque session cookie (no token cookies)",fn:async()=>{let Z=await b$($,Q);if(!Z.passed)throw Error(Z.error)}}],createLogoutTests:()=>[{name:"logout endpoint clears cookies",fn:async()=>{let Z=await O$($,Q);if(!Z.passed)throw Error(Z.error)}}],createBackChannelLogoutTests:()=>[{name:"endpoint exists",fn:async()=>{let Z=await M$($,Q);if(!Z.passed)throw Error(Z.error)}}],createCallbackErrorSurfacedTests:(Z)=>[{name:"callback error surfaced",fn:async()=>{let G=await jQ($,Q,Z);if(!G.passed)throw Error(G.error)}}]}}}import{ION_TENANT_MANAGE as qQ}from"@ionite/core";import{InMemoryTenantStore as FQ,InMemoryWorkloadTokenStore as h$,ionite as C$,tenantManager as LQ,validateWorkloadToken as VQ}from"@ionite/server";import{createValidators as A$}from"@ionite/valibot";function S$(X){return{async load(){return X},async subscribe(){return},secret:{}}}function k$(){return{id:"sso",workload:{incoming:[{issuer:"http://idp.example/api/t/master",jwksUri:"http://idp.example/jwks",audience:["eci"]}]},authz:{trustedProvisioningClients:[{issuer:"http://idp.example/api/t/master",audience:["eci"],clientId:"eci",permissions:[qQ.id]}]}}}function _2(){return{tests:[{name:"SSO-shaped ionite TM rejects tenant create without a workload token",fn:async()=>{let $=A$(),Q=C$(S$(k$()),{validators:$,stores:{workloadTokenStore:new h$}});await Q.ready();let Y=await LQ(Q,{basePath:"/api/tenants",store:new FQ,validators:$}).handler(new Request("http://sso.example/api/tenants",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:"acme",companyId:"acme",companyName:"Acme",environmentType:"DEV"})}));z(Y.status===401,`expected 401 without workload auth, got ${Y.status}`),await Q.close()}},{name:"SSO-shaped client registration rejects a request with no workload token",fn:async()=>{let $=C$(S$(k$()),{validators:A$(),stores:{workloadTokenStore:new h$}});await $.ready();let Q=await VQ(new Request("http://sso.example/api/admin/tenants/acme/clients",{method:"POST"}),$);z(Q.valid===!1,"missing bearer must not validate"),await $.close()}}]}}async function NQ(){return _2()}import*as P1 from"node:crypto";import{clearJwksCache as U2,setJwksClock as I5,setJwksRetry as I$,verifyIdToken as K0,verifyLogoutToken as R2}from"@ionite/core/server";import{createValidators as OQ}from"@ionite/valibot";var l="https://idp.ecv.local/realms/ecv",f="ecv-oidc-client",MQ="http://schemas.openid.net/event/backchannel-logout";function t(X){let{publicKey:$,privateKey:Q}=P1.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function S5(X){return Buffer.from(X).toString("base64url")}function o(X){let $={alg:X.headerAlg??"RS256",typ:"JWT"};if(X.headerKid!==null)$.kid=X.headerKid??X.key.kid;let Q=`${S5(JSON.stringify($))}.${S5(JSON.stringify(X.claims))}`,K=P1.createSign("RSA-SHA256");return K.update(Q),`${Q}.${S5(K.sign(X.key.privateKey))}`}function $0(X={}){let $=Math.floor(Date.now()/1000);return{iss:l,aud:f,sub:"ecv-user",iat:$,exp:$+3600,...X}}async function J0(X,$,Q){let K=globalThis.fetch;U2(X),globalThis.fetch=async(Y,Z)=>{if((typeof Y==="string"?Y:Y.toString())===X)return new Response(JSON.stringify($),{status:200,headers:{"Content-Type":"application/json"}});return K(Y,Z)};try{return await Q()}finally{globalThis.fetch=K,U2(X)}}async function U0(X,$,Q){try{await X()}catch(K){let Y=K instanceof Error?K.message:String(K);if(!Y.includes($))throw Error(`${Q}: expected error containing "${$}", got "${Y}"`);return}throw Error(`${Q}: expected rejection, but the call resolved`)}function Q0(){return`https://jwks.ecv.local/${P1.randomUUID()}`}function k5(X={}){let $=Math.floor(Date.now()/1000);return{iss:l,aud:f,iat:$,jti:P1.randomUUID(),sid:"ecv-session-1",events:{[MQ]:{}},...X}}async function v2(X,$,Q){let K=globalThis.fetch;U2(X),I$({maxRetries:0}),globalThis.fetch=async(Y,Z)=>{if((typeof Y==="string"?Y:Y.toString())===X){let W=$();if(W==="fail")return new Response("unavailable",{status:503});return new Response(JSON.stringify(W),{status:200,headers:{"Content-Type":"application/json"}})}return K(Y,Z)};try{return await Q()}finally{globalThis.fetch=K,U2(X),I5(),I$()}}function b2(){return{tests:[{name:"valid ID token (correct iss/aud/exp/kid) resolves claims",fn:async()=>{let $=t("ecv-valid-kid"),Q=Q0(),K=o({key:$,claims:$0()}),Y=await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}));if(Y.sub!=="ecv-user")throw Error(`expected sub "ecv-user", got "${String(Y.sub)}"`)}},{name:"wrong aud and wrong azp throws audience mismatch",fn:async()=>{let $=t("ecv-aud-kid"),Q=Q0(),K=o({key:$,claims:$0({aud:"someone-else",azp:"someone-else"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"audience mismatch","wrong aud/azp"))}},{name:"matching azp only (client absent from aud) passes",fn:async()=>{let $=t("ecv-azp-kid"),Q=Q0(),K=o({key:$,claims:$0({aud:"api://resource-server",azp:f})}),Y=await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}));if(Y.azp!==f)throw Error(`expected azp "${f}", got "${String(Y.azp)}"`)}},{name:"wrong iss throws issuer mismatch",fn:async()=>{let $=t("ecv-iss-kid"),Q=Q0(),K=o({key:$,claims:$0({iss:"https://attacker.example/realms/evil"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"issuer mismatch","wrong iss"))}},{name:"expired exp throws (beyond clock skew)",fn:async()=>{let $=t("ecv-exp-kid"),Q=Q0(),K=Math.floor(Date.now()/1000)-3600,Y=o({key:$,claims:$0({exp:K})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:Y,jwksUri:Q,issuer:l,audience:f}),"expired","expired token"))}},{name:"missing kid in header throws",fn:async()=>{let $=t("ecv-nokid-kid"),Q=Q0(),K=o({key:$,headerKid:null,claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"missing kid","missing kid"))}},{name:"unknown header kid fails closed after JWKS refresh (no first-key fallback)",fn:async()=>{let $=t("ecv-realkid"),Q=Q0(),K=o({key:$,headerKid:"kid-not-in-jwks",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Public key not found","unknown kid"))}},{name:"JWKS rotation: a token signed with a newly published kid verifies after refresh (§1.10)",fn:async()=>{let $=t("kid-1"),Q=t("kid-2"),K=Q0(),Y=[$.jwk];await v2(K,()=>({keys:Y}),async()=>{if(await K0({idToken:o({key:$,claims:$0()}),jwksUri:K,issuer:l,audience:f}),Y=[$.jwk,Q.jwk],(await K0({idToken:o({key:Q,claims:$0()}),jwksUri:K,issuer:l,audience:f})).sub!=="ecv-user")throw Error("rotated-key token did not verify")})}},{name:"JWKS 503 on a cold cache fails closed (§1.29)",fn:async()=>{let $=t("kid-cold"),Q=Q0(),K=o({key:$,claims:$0()});await v2(Q,()=>"fail",()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Failed to fetch JWKS","cold JWKS 503"))}},{name:"JWKS TTL expiry triggers a refresh that picks up rotation (§1.10)",fn:async()=>{let $=t("kid-ttl-1"),Q=t("kid-ttl-2"),K=Q0(),Y=Date.UTC(2026,0,1);I5(()=>Y);let Z=[$.jwk];await v2(K,()=>({keys:Z}),async()=>{if(await K0({idToken:o({key:$,claims:$0()}),jwksUri:K,issuer:l,audience:f}),Z=[Q.jwk],Y+=660000,(await K0({idToken:o({key:Q,claims:$0()}),jwksUri:K,issuer:l,audience:f})).sub!=="ecv-user")throw Error("post-TTL rotated token did not verify")})}},{name:"JWKS warm refresh failure serves last-known-good keys (§1.10)",fn:async()=>{let $=t("kid-warm"),Q=Q0(),K=Date.UTC(2026,0,1);I5(()=>K);let Y="ok";await v2(Q,()=>Y==="fail"?"fail":{keys:[$.jwk]},async()=>{if(await K0({idToken:o({key:$,claims:$0()}),jwksUri:Q,issuer:l,audience:f}),K+=660000,Y="fail",(await K0({idToken:o({key:$,claims:$0()}),jwksUri:Q,issuer:l,audience:f})).sub!=="ecv-user")throw Error("last-known-good keys were not served on refresh failure")})}},{name:"logout_token with events + sid (no nonce) verifies (§1.6)",fn:async()=>{let $=t("kid-logout-ok"),Q=Q0(),K=o({key:$,claims:k5()}),Y=await J0(Q,{keys:[$.jwk]},()=>R2({logoutToken:K,jwksUri:Q,issuer:l,audience:f}));if(Y.sid!=="ecv-session-1")throw Error(`expected sid, got "${String(Y.sid)}"`)}},{name:"logout_token containing a nonce is rejected (§1.6)",fn:async()=>{let $=t("kid-logout-nonce"),Q=Q0(),K=o({key:$,claims:k5({nonce:"must-not-be-here"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>R2({logoutToken:K,jwksUri:Q,issuer:l,audience:f}),"must not contain a nonce","logout_token nonce"))}},{name:"plain ID token (no events) rejected as a logout_token (§1.6)",fn:async()=>{let $=t("kid-logout-idtoken"),Q=Q0(),K=o({key:$,claims:$0({sid:"ecv-session-1"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>R2({logoutToken:K,jwksUri:Q,issuer:l,audience:f}),"missing back-channel logout event","id token as logout_token"))}},{name:"logout_token missing both sid and sub is rejected (§1.6)",fn:async()=>{let $=t("kid-logout-nosub"),Q=Q0(),K=o({key:$,claims:k5({sid:void 0,sub:void 0})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>R2({logoutToken:K,jwksUri:Q,issuer:l,audience:f}),"missing sid and sub","logout_token no sid/sub"))}},{name:"nonce: matching expectedNonce at the callback exchange passes (§1.8)",fn:async()=>{let $=t("ecv-nonce-ok"),Q=Q0(),K=o({key:$,claims:$0({nonce:"ecv-nonce-123"})});if((await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f,expectedNonce:"ecv-nonce-123"}))).nonce!=="ecv-nonce-123")throw Error("expected the nonce claim to round-trip")}},{name:"nonce: a different nonce claim is rejected when expectedNonce is set (§1.8)",fn:async()=>{let $=t("ecv-nonce-bad"),Q=Q0(),K=o({key:$,claims:$0({nonce:"attacker-nonce"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f,expectedNonce:"ecv-nonce-123"}),"nonce mismatch","nonce mismatch"))}},{name:"nonce: a MISSING nonce claim is rejected when expectedNonce is set (§1.8)",fn:async()=>{let $=t("ecv-nonce-missing"),Q=Q0(),K=o({key:$,claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f,expectedNonce:"ecv-nonce-123"}),"nonce mismatch","missing nonce claim"))}},{name:"nonce: refresh-path verification (no expectedNonce) accepts a token without a nonce (§1.8)",fn:async()=>{let $=t("ecv-nonce-refresh"),Q=Q0(),K=o({key:$,claims:$0()});if((await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}))).sub!=="ecv-user")throw Error("nonce-less refresh verification failed")}},{name:'alg pinning: header alg "none" is rejected before key resolution (§1.9)',fn:async()=>{let $=t("ecv-alg-none"),Q=Q0(),K=o({key:$,headerAlg:"none",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Unsupported JWT alg","alg none"))}},{name:"alg pinning: HMAC header alg (HS256) is rejected (§1.9)",fn:async()=>{let $=t("ecv-alg-hs"),Q=Q0(),K=o({key:$,headerAlg:"HS256",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Unsupported JWT alg","alg HS256"))}},{name:"alg pinning: an RSA JWK cannot verify a token claiming ES256 (§1.9)",fn:async()=>{let $=t("ecv-alg-cross"),Q=Q0(),K=o({key:$,headerAlg:"ES256",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"does not match token alg","alg/JWK-type cross"))}},{name:"token response validator rejects a body missing access_token (§1.11 refresh fails closed)",fn:async()=>{let $=OQ();if(!(await $.sso.tokenResponse.validate({token_type:"Bearer"})).issues)throw Error("expected validation issues for a token response missing access_token");if((await $.sso.tokenResponse.validate({access_token:"a.b.c",id_token:"d.e.f",token_type:"Bearer"})).issues)throw Error("expected a minimal valid token response to pass validation")}}]}}async function TQ(){let X=performance.now(),{tests:$}=b2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"SSO ID token verification",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import*as z1 from"node:crypto";import{defaultLogger as xQ}from"@ionite/core";import{clearJwksCache as m$,sso as _Q}from"@ionite/core/server";import{InMemorySessionStore as RQ}from"@ionite/server";import{createValidators as vQ}from"@ionite/valibot";var f$="https://idp.ecv.local/realms/ecv",p$="ecv-oidc-client",g$="ion.sso",p5=86400,m5=7*p5,UQ=5000;function bQ(X){let{publicKey:$,privateKey:Q}=z1.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function f5(X){return Buffer.from(X).toString("base64url")}function yQ(X,$){let Q={alg:"RS256",typ:"JWT",kid:X.kid},K=`${f5(JSON.stringify(Q))}.${f5(JSON.stringify($))}`,Y=z1.createSign("RSA-SHA256");return Y.update(K),`${K}.${f5(Y.sign(X.privateKey))}`}function u$(X={}){let $=Math.floor(Date.now()/1000);return{iss:f$,aud:p$,sub:"ecv-user",iat:$,exp:$+3600,...X}}function h2(X,$){if(!X)throw Error($)}function EQ(X){return new Request("https://app.ecv.local/",{headers:{cookie:`${g$}.session=${X}`}})}function hQ(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"old-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"old-refresh",expires:new Date(Date.now()-1000).toISOString()},claims:u$({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}async function y2(X){let $=bQ(`kid-${z1.randomUUID()}`),Q=`https://jwks.ecv.local/${z1.randomUUID()}`,K=`https://idp.ecv.local/token/${z1.randomUUID()}`,Y=new RQ,Z=[],G={async create(H){Z.push(H.sid),await Y.create(H)},get:(H)=>Y.get(H),update:(H,J)=>Y.update(H,J),delete:(H)=>Y.delete(H)};await G.create(hQ("sid-window"));let W=globalThis.fetch;m$(Q),globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H.toString();if(j===Q)return new Response(JSON.stringify({keys:[$.jwk]}),{status:200,headers:{"Content-Type":"application/json"}});if(j===K){let D=yQ($,u$({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}));return new Response(JSON.stringify({access_token:"new-access",id_token:D,token_type:"Bearer",refresh_token:"new-refresh",expires_in:300,...X.refreshExpiresIn!==void 0?{refresh_expires_in:X.refreshExpiresIn}:{},scope:"openid"}),{status:200,headers:{"Content-Type":"application/json"}})}return W(H,J)};try{let H=vQ(),J=_Q(H.sso,xQ,void 0,{authority:f$,tokenUrl:K,authorizationUrl:"https://idp.ecv.local/authorize",clientId:p$,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Q,sessionStore:G,cookiesPrefix:g$,cookiesSecure:!1,sessionWindowSeconds:X.sessionWindowSeconds},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!J)throw Error("sso() returned undefined");let j=Date.now(),D=await J.resolveUser(EQ("sid-window"));h2(D.status==="authenticated",`refresh should succeed, got ${D.status}`);let P=Z.find((q)=>q!=="sid-window");h2(!!P,"refresh must rotate to a new sid");let B=await G.get(P);return h2(B!=null,"rotated session record must exist"),new Date(B.expiresAt).getTime()-j}finally{globalThis.fetch=W,m$(Q)}}function E2(X,$,Q){let K=$*1000;h2(Math.abs(X-K)<=UQ,`${Q}: expected a ~${$}s session window, got ${Math.round(X/1000)}s`)}function C2(){return{tests:[{name:"IdP refresh_expires_in always wins over the configured fallback",fn:async()=>{let $=await y2({sessionWindowSeconds:m5,refreshExpiresIn:3600});E2($,3600,"IdP-stated window must override sessionWindowSeconds")}},{name:"configured sessionWindowSeconds is honored when the IdP omits refresh_expires_in (no 24h cap)",fn:async()=>{let $=await y2({sessionWindowSeconds:m5});E2($,m5,"a window longer than the 24h default must be honored")}},{name:"defaults to 86400s when unset and the IdP omits refresh_expires_in",fn:async()=>{let $=await y2({});E2($,p5,"default fallback window")}},{name:"non-positive sessionWindowSeconds falls back to the 86400s default",fn:async()=>{let $=await y2({sessionWindowSeconds:0});E2($,p5,"a nonsense (<1) value must not produce an instant-death window")}}]}}async function CQ(){let X=performance.now(),{tests:$}=C2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"SSO session window",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import*as p0 from"node:crypto";import{defaultLogger as AQ}from"@ionite/core";import{clearJwksCache as l$,sso as SQ}from"@ionite/core/server";import{InMemorySessionStore as n$}from"@ionite/server";import{createValidators as kQ}from"@ionite/valibot";var i$="https://idp.ecv.local/realms/ecv",S2="ecv-oidc-client",A2="ecv-confidential-secret",o$="ion.sso";function IQ(X){let{publicKey:$,privateKey:Q}=p0.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function g5(X){return Buffer.from(X).toString("base64url")}function mQ(X,$){let Q={alg:"RS256",typ:"JWT",kid:X.kid},K=`${g5(JSON.stringify(Q))}.${g5(JSON.stringify($))}`,Y=p0.createSign("RSA-SHA256");return Y.update(K),`${K}.${g5(Y.sign(X.privateKey))}`}function u5(X={}){let $=Math.floor(Date.now()/1000);return{iss:i$,aud:S2,sub:"ecv-user",iat:$,exp:$+3600,...X}}function M0(X,$){if(!X)throw Error($)}function a$(X){return new Request("https://app.ecv.local/",{headers:{cookie:`${o$}.session=${X}`}})}function fQ(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"old-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"old-refresh",expires:new Date(Date.now()-1000).toISOString()},claims:u5({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}function pQ(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"live-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"live-refresh",expires:new Date(Date.now()+3600000).toISOString()},claims:u5({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}function t$(X,$){let Q=kQ(),K=SQ(Q.sso,AQ,void 0,{authority:i$,tokenUrl:$.tokenUrl,authorizationUrl:"https://idp.ecv.local/authorize",clientId:S2,clientSecret:$.clientSecret,revocationEndpoint:$.revocationEndpoint,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:$.jwksUri,sessionStore:X,cookiesPrefix:o$,cookiesSecure:!1},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!K)throw Error("sso() returned undefined");return K}async function d$(X){let $=IQ(`kid-${p0.randomUUID()}`),Q=`https://jwks.ecv.local/${p0.randomUUID()}`,K=`https://idp.ecv.local/token/${p0.randomUUID()}`,Y=new n$;await Y.create(fQ("sid-refresh"));let Z=globalThis.fetch;l$(Q);let G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W.toString();if(J===Q)return new Response(JSON.stringify({keys:[$.jwk]}),{status:200,headers:{"Content-Type":"application/json"}});if(J===K){G=String(H?.body??"");let j=mQ($,u5({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}));return new Response(JSON.stringify({access_token:"new-access",id_token:j,token_type:"Bearer",refresh_token:"new-refresh",expires_in:3600,refresh_expires_in:86400,scope:"openid"}),{status:200,headers:{"Content-Type":"application/json"}})}return Z(W,H)};try{let H=await t$(Y,{tokenUrl:K,jwksUri:Q,clientSecret:X}).resolveUser(a$("sid-refresh"));return M0(H.status==="authenticated",`refresh should succeed, got ${H.status}`),M0(G!==void 0,"refresh must POST to the token endpoint"),new URLSearchParams(G)}finally{globalThis.fetch=Z,l$(Q)}}async function c$(X){let $=`https://jwks.ecv.local/${p0.randomUUID()}`,Q=`https://idp.ecv.local/token/${p0.randomUUID()}`,K=`https://idp.ecv.local/revoke/${p0.randomUUID()}`,Y=new n$;await Y.create(pQ("sid-logout"));let Z=globalThis.fetch,G;globalThis.fetch=async(W,H)=>{if((typeof W==="string"?W:W.toString())===K)return G=String(H?.body??""),new Response(null,{status:200});return Z(W,H)};try{return await t$(Y,{tokenUrl:Q,jwksUri:$,clientSecret:X,revocationEndpoint:K}).logout(a$("sid-logout")),M0(G!==void 0,"logout must POST the refresh token to the revocation endpoint"),new URLSearchParams(G)}finally{globalThis.fetch=Z}}function k2(){return{tests:[{name:"confidential client: refresh_token grant authenticates with client_secret (RFC 6749 §6)",fn:async()=>{let $=await d$(A2);M0($.get("grant_type")==="refresh_token","grant_type must be refresh_token"),M0($.get("refresh_token")==="old-refresh","refresh must send the stored refresh token"),M0($.get("client_id")===S2,"refresh must send client_id"),M0($.get("client_secret")===A2,"a confidential client must send client_secret on refresh — omitting it makes Keycloak reject with unauthorized_client at the first token expiry")}},{name:"public client: refresh_token grant sends no client_secret",fn:async()=>{let $=await d$(void 0);M0($.get("grant_type")==="refresh_token","grant_type must be refresh_token"),M0($.get("client_secret")===null,"a public (PKCE) client must not send a client_secret")}},{name:"confidential client: token revocation authenticates with client_secret (RFC 7009 §2.1)",fn:async()=>{let $=await c$(A2);M0($.get("token")==="live-refresh","revocation must send the stored refresh token"),M0($.get("token_type_hint")==="refresh_token","revocation must hint refresh_token"),M0($.get("client_id")===S2,"revocation must send client_id"),M0($.get("client_secret")===A2,"a confidential client must send client_secret on revocation — otherwise the refresh token silently outlives logout")}},{name:"public client: token revocation sends no client_secret",fn:async()=>{let $=await c$(void 0);M0($.get("token")==="live-refresh","revocation must send the stored refresh token"),M0($.get("client_secret")===null,"a public (PKCE) client must not send a client_secret")}}]}}async function gQ(){let X=performance.now(),{tests:$}=k2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"SSO token client auth",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{trimTrailingSlash as r$}from"@ionite/server";async function uQ(X){try{return await X.json()}catch{return{}}}async function c1(X){let $=r$(X.tenantPath??"/api/tenants"),Q=X.profile??"tenantManager",K=await k0(X.ecv.baseUrl,f1),Y={...X.tenant,configLocator:X.tenant.configLocator??X.ecv.configLocator,webhookUrl:X.tenant.webhookUrl??`${X.ecv.baseUrl}/webhook`},Z=await fetch(new URL($,X.baseUrl),{method:"POST",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/json"},body:JSON.stringify(Y),signal:AbortSignal.timeout(X.readiness?.requestTimeoutMs??15000)}),G=await uQ(Z);if(Z.status===409||G.status==="failed")throw Error(`Tenant ${Y.id} provisioning failed: ${String(G.error??Z.statusText)}`);if(G.status==="action_required")throw Error(`Tenant ${Y.id} provisioning requires action at ${G.actionUrl}`);if(z(Z.status===200||Z.status===201||Z.status===202,`Expected tenant create to return 200, 201, or 202, got ${Z.status}`),z(typeof G.tenantUrl==="string",`Tenant ${Y.id} response must include tenantUrl`),Q==="creation"){let B=r$(G.tenantUrl),q=X.basePath??"/api/ionite";return await E1(B,{...X.readiness,basePath:q}),{id:Y.id,baseUrl:B,basePath:q,tenantUrl:G.tenantUrl}}let W=await fetch(new URL(`${$}/${encodeURIComponent(Y.id)}`,X.baseUrl),{headers:{Authorization:`Bearer ${K}`},signal:AbortSignal.timeout(X.readiness?.requestTimeoutMs??15000)});z(W.status===200,`Expected tenant GET to return 200, got ${W.status}`);let H=await W.json();z(H.id===Y.id,`Expected tenant GET id ${Y.id}`),z(typeof H.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl"),z(typeof H.ionUrls.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl");let J=new URL(H.ionUrls.capabilitiezUrl),j="/capabilitiez";z(J.pathname.endsWith(j),"Tenant capabilitiezUrl must end in /capabilitiez");let D=J.pathname.slice(0,-j.length)||"/",P=new URL(H.ionUrls.readyzUrl);return await E1(J.origin,{...X.readiness,readyPath:`${P.pathname}${P.search}`}),{id:Y.id,baseUrl:J.origin,basePath:D,tenantUrl:typeof H.tenantUrl==="string"?H.tenantUrl:G.tenantUrl}}var s$={tenantPath:"/api/tenants"};function l5(X,$){return`${(X.tenantPath??"/api/tenants").replace(/\/+$/,"")}/${$}`}async function e$(X,$,Q,K){return N("Tenant Create",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let Y={id:Q.id,companyId:Q.companyId,companyName:Q.companyName,environmentType:Q.environmentType,...Q.configLocator?{configLocator:Q.configLocator}:{},...Q.email?{email:Q.email}:{},...Q.webhookUrl||K?{webhookUrl:Q.webhookUrl??K}:{},...Q.callbackUrl?{callbackUrl:Q.callbackUrl}:{}},Z=await $(X.tenantPath??"/api/tenants",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Y)});z(Z.status===200||Z.status===201||Z.status===202,`Expected 200, 201, or 202, got ${Z.status}`);let G=await Z.json();z(typeof G.status==="string","Expected response status");let W=G.status;if(z(["action_required","pending","processing","completed","failed"].includes(W),`Unexpected tenant status: ${W}`),W==="action_required")z(typeof G.actionUrl==="string","Expected actionUrl for action_required"),z(typeof G.requestToken==="string","Expected requestToken for action_required"),z(typeof G.expiresAt==="string","Expected expiresAt for action_required");else if(G.tenantUrl!=null)z(typeof G.tenantUrl==="string","Expected tenantUrl to be a string when provided");return{details:{httpStatus:Z.status}}})}async function $4(X,$,Q){return N("Tenant Get",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let K=await $(l5(X,Q.id),{method:"GET",headers:{"Content-Type":"application/json"}});z(K.status===200,`Expected 200, got ${K.status}`);let Y=await K.json();return z(Y.id===Q.id,"Expected id to match"),{details:{httpStatus:K.status}}})}async function Q4(X,$,Q){return N("Tenant Delete",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let K=await $(l5(X,Q.id),{method:"DELETE",headers:{"Content-Type":"application/json"}});z(K.status===204,`Expected 204, got ${K.status}`);let Y=await $(l5(X,Q.id),{method:"GET"});return z(Y.status===404,`Expected 404 after delete, got ${Y.status}`),{details:{httpStatus:K.status}}})}async function K4(X,$){return N("Tenant Creation (Validation)",async()=>{let Q={companyId:"test-company"},K=X.tenantPath??"/api/tenants",Y=await $(K,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Q)});return z(Y.status===400,`Expected 400 Bad Request for invalid payload, got ${Y.status}`),{details:{status:Y.status}}})}async function Y4(X,$){return N("Tenant Creation Rejects Malformed JSON",async()=>{let Q=X.tenantPath??"/api/tenants",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/json"},body:'{"companyId":'});return z(K.status===400,`Expected 400 Bad Request for malformed JSON, got ${K.status}`),{details:{status:K.status}}})}async function Z4(X,$,Q){return N("Tenant Create Without Config Locator",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let K=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),Y=`${Q.id}-unconfigured`;try{let Z=await $(K,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:Y,companyId:Q.companyId,companyName:Q.companyName,environmentType:Q.environmentType})});z(Z.status===200||Z.status===201||Z.status===202,`Expected create without configLocator to succeed, got ${Z.status}`);let G=await $(`${K}/${encodeURIComponent(Y)}`,{method:"GET"});z(G.status===200,`Expected tenant GET 200, got ${G.status}`);let W=await G.json();return z(W.configLocator==null,"Expected configLocator to remain absent after create"),{details:{httpStatus:Z.status}}}finally{await $(`${K}/${encodeURIComponent(Y)}`,{method:"DELETE"}).catch(()=>{return})}})}async function X4(X,$,Q){return N("Tenant Config Locator PUT Patch",async()=>{if(!Q?.configLocator)return{details:{skipped:!0,reason:"Test tenant config locator not provided"}};let K=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),Y=`${Q.id}-locator-patch`;try{let Z=await $(K,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:Y,companyId:Q.companyId,companyName:Q.companyName,environmentType:Q.environmentType})});z(Z.status===200||Z.status===201||Z.status===202,`Expected initial tenant create to succeed, got ${Z.status}`);let G=await $(`${K}/${encodeURIComponent(Y)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({configLocator:Q.configLocator})});z(G.status===200||G.status===201||G.status===202,`Expected configLocator PUT to return 200, 201, or 202, got ${G.status}`);let W=await $(`${K}/${encodeURIComponent(Y)}`,{method:"GET"});z(W.status===200,`Expected tenant GET 200, got ${W.status}`);let H=await W.json();return z(JSON.stringify(H.configLocator)===JSON.stringify(Q.configLocator),"Expected PUT to persist the complete configLocator"),z(H.companyName===Q.companyName,"Expected PUT patch to preserve existing tenant fields"),{details:{httpStatus:G.status}}}finally{await $(`${K}/${encodeURIComponent(Y)}`,{method:"DELETE"}).catch(()=>{return})}})}async function z4(X,$){return N("Tenant Rejects Legacy user-tenants Route",async()=>{let Q=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),K=await $(`${Q}/user-tenants?email=test@example.com`,{method:"GET",headers:{"Content-Type":"application/json"}});return z(K.status===404,`Expected 404, got ${K.status}`),{details:{status:K.status}}})}async function lQ(X){let $=performance.now(),Q={...s$,...X},K=r(Q),Y=Q.profile??"tenantManager",Z=[];if(Z.push(await G4(Q)),Z.push(await W4(Q)),Z.push(await K4(Q,K)),Z.push(await Y4(Q,K)),Y==="tenantManager")Z.push(await z4(Q,K)),Z.push(await Z4(Q,K,Q.testTenantData)),Z.push(await X4(Q,K,Q.testTenantData));if(Z.push(await e$(Q,K,Q.testTenantData,Q.webhookPath?`${T0(Q)}${Q.webhookPath}`:void 0)),Y==="tenantManager")Z.push(await $4(Q,K,Q.testTenantData)),Z.push(await Q4(Q,K,Q.testTenantData));let G=performance.now()-$,W=Z.filter((j)=>j.passed).length,H=Z.filter((j)=>!j.passed).length,J=Z.filter((j)=>j.details?.skipped).length;return{suite:"Tenant",passed:H===0,tests:Z,duration:G,summary:{total:Z.length,passed:W-J,failed:H,skipped:J}}}async function G4(X){return N("Tenant Rejects Without Workload Auth",async()=>{let $=X.tenantPath??"/api/tenants",K=await r({baseUrl:X.baseUrl,timeout:X.timeout,headers:{}})($,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:"auth-test-app",companyId:"auth-test-company",companyName:"Auth Test Company",environmentType:"POC",configLocator:{type:"vault",vaultPath:"/ecv/test-config"}})});return z(K.status===401,`Expected 401 without workload auth, got ${K.status}`),{details:{httpStatus:K.status}}})}async function W4(X){return N("Tenant Rejects Invalid Workload Token",async()=>{let $=X.tenantPath??"/api/tenants",K=await r({baseUrl:X.baseUrl,timeout:X.timeout,headers:{Authorization:"Bearer invalid-token"}})($,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:"auth-test-app",companyId:"auth-test-company",companyName:"Auth Test Company",environmentType:"POC",configLocator:{type:"vault",vaultPath:"/ecv/test-config"}})});return z(K.status===401,`Expected 401 with invalid token, got ${K.status}`),{details:{httpStatus:K.status}}})}function H4(X){let $={...s$,...X},Q=r($),K=$.profile??"tenantManager",Y=[{name:"tenant creation endpoint rejects requests without workload authentication",fn:async()=>{let Z=await G4($);if(!Z.passed)throw Error(Z.error)}},{name:"tenant creation endpoint rejects requests with invalid workload token",fn:async()=>{let Z=await W4($);if(!Z.passed)throw Error(Z.error)}},{name:"tenant creation endpoint validates required fields",fn:async()=>{let Z=await K4($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"tenant creation endpoint rejects malformed JSON",fn:async()=>{let Z=await Y4($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"tenant manager rejects legacy user-tenants route",fn:async()=>{let Z=await z4($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"tenant create accepts an omitted config locator",fn:async()=>{let Z=await Z4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant PUT attaches a config locator without replacing other fields",fn:async()=>{let Z=await X4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant upsert returns valid response structure",fn:async()=>{let Z=await e$($,Q,$.testTenantData,$.webhookPath?`${T0($)}${$.webhookPath}`:void 0);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant get returns upserted tenant",fn:async()=>{let Z=await $4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant delete removes tenant",fn:async()=>{let Z=await Q4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}}];if(K==="creation")return Y.filter((Z)=>["tenant creation endpoint rejects requests without workload authentication","tenant creation endpoint rejects requests with invalid workload token","tenant creation endpoint validates required fields","tenant creation endpoint rejects malformed JSON","tenant upsert returns valid response structure"].includes(Z.name));return Y}function n1(X){return typeof X.ecv==="function"?X.ecv():X.ecv}function I2(X){return`${X}-${Date.now()}-${Math.random().toString(36).slice(2,9)}`}function m2(X,$){return{id:$,companyId:$,companyName:`ECV ${$}`,environmentType:"DEV",get configLocator(){return{type:"vault",vaultPath:`${n1(X).configLocator.vaultPath}/${$}`}},get webhookUrl(){return`${n1(X).baseUrl}/webhook`}}}async function c5(X){return k0(n1(X).baseUrl,f1)}async function d5(X,$,Q){let K=(X.tenantPath??"/api/tenants").replace(/\/+$/,"");return fetch(new URL(`${K}/${encodeURIComponent($)}`,T0(X)),{method:"DELETE",headers:{Authorization:`Bearer ${Q}`},signal:AbortSignal.timeout(X.timeout??15000)})}function dQ(X){let $=m2(X,I2("ecv-lifecycle"));return{name:"tenant lifecycle exposes derived URLs and tears down tenant routing",fn:async()=>{let Q=n1(X),K=await c5(X);try{let Y=await c1({ecv:Q,baseUrl:T0(X),tenantPath:X.tenantPath,tenant:$,readiness:{requestTimeoutMs:X.timeout??15000}}),Z=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),G=await fetch(new URL(`${Z}/${encodeURIComponent($.id)}`,T0(X)),{headers:{Authorization:`Bearer ${K}`},signal:AbortSignal.timeout(X.timeout??15000)});z(G.status===200,`Expected tenant GET 200, got ${G.status}`);let W=await G.json();z(typeof W.callbackUrl==="string","Tenant GET must include callbackUrl"),z(typeof W.ionUrls?.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl"),z(typeof W.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl");let H=await d5(X,$.id,K);z(H.status===204,`Expected tenant DELETE 204, got ${H.status}`);let J=await fetch(new URL(`${Z}/${encodeURIComponent($.id)}`,T0(X)),{headers:{Authorization:`Bearer ${K}`},signal:AbortSignal.timeout(X.timeout??15000)});z(J.status===404,`Expected tenant GET 404 after delete, got ${J.status}`);let j=await fetch(`${Y.baseUrl}${Y.basePath}/readyz`,{signal:AbortSignal.timeout(X.timeout??15000)});if(z(j.status===503,`Expected deleted tenant route to return 503, got ${j.status}`),Y.basePath.endsWith("/ionite")){let D=`${Y.basePath.slice(0,-7)}/readyz`,P=await fetch(`${Y.baseUrl}${D}`,{signal:AbortSignal.timeout(X.timeout??15000)});z(P.status===404,`Expected deleted tenant readiness to return 404, got ${P.status}`)}}finally{await d5(X,$.id,K).catch(()=>{return})}}}}function cQ(X){let $=m2(X,I2("ecv-isolation-a")),Q=m2(X,I2("ecv-isolation-b"));return{name:"session from one tenant cannot authenticate against another tenant",fn:async()=>{let K=n1(X),Y=await c5(X),Z=[];try{Z.push($.id);let G=await c1({ecv:K,baseUrl:T0(X),tenantPath:X.tenantPath,tenant:$,readiness:{requestTimeoutMs:X.timeout??15000}});Z.push(Q.id);let W=await c1({ecv:K,baseUrl:T0(X),tenantPath:X.tenantPath,tenant:Q,readiness:{requestTimeoutMs:X.timeout??15000}}),H={baseUrl:G.baseUrl,timeout:X.timeout,loginPath:`${G.basePath}/auth/login`,callbackPath:`${G.basePath}/auth/callback`,userPath:`${G.basePath}/auth/user`},J=r(H),j=await d1(H,J);z(j.success,j.error??"Tenant A login failed");let D=f0(j.cookies);z(D.length>0,"Tenant A login did not produce a session cookie");let P=await J(H.userPath,{headers:{Cookie:D}});z(P.status===200,`Tenant A session did not authenticate on tenant A: ${P.status}`);let q=await r({baseUrl:W.baseUrl,timeout:X.timeout})(`${W.basePath}/auth/user`,{headers:{Cookie:D}});z(q.status===401,`Tenant A session authenticated on tenant B (expected 401, got ${q.status})`)}finally{await Promise.all(Z.map((G)=>d5(X,G,Y).catch(()=>{return})))}}}}function n5(X){let $=X.profile??"tenantManager",Q,K=m2(X,I2("ecv-contract")),Y=H4({baseUrl:X.baseUrl,timeout:X.timeout,tenantPath:X.tenantPath,profile:$,webhookPath:X.webhookPath,headers:async()=>{return Q??=c5(X),{Authorization:`Bearer ${await Q}`}},testTenantData:K});if($==="creation")return Y;return[...Y,dQ(X),cQ(X)]}import{defaultLogger as nQ,list as iQ,remoteConfigSecretType as i5}from"@ionite/core";import{generateLfvEd25519KeyPair as i1,signLfvPayload as j4,stringifyLfvJwks as f2}from"@ionite/core/server";import{ionite as p2,lfvVault as o1,mirroredConfig as oQ,mirrorSecrets as aQ,tenantMirror as tQ}from"@ionite/server";import{createValidators as g2}from"@ionite/valibot";var z0={lfvServerUrl:"http://localhost:3599/api/lfv",clientId:"ecv-coordination-client",path:"/ecv/coordination/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"};class u2{values=new Map;async put(X,$,Q){this.values.set(X,$)}async take(X){let $=this.values.get(X);return this.values.delete(X),$}}async function a1(X,$,Q,K){let Y="issued_at"in $?$:{...$,issued_at:Date.now()},Z=JSON.stringify(Y),G=String($.request_id),W=await j4({privateKeyJwk:Q,requestId:G,rawBody:Z});return new Request(`http://localhost${X}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":G,"X-LFV-Key-Id":K,"X-LFV-Signature":W},body:Z})}async function J4(X,$,Q){let K=String(X.request_id),Y=JSON.stringify(X),Z=await j4({privateKeyJwk:$,requestId:K,rawBody:Y});return new Response(Y,{status:200,headers:{"Content-Type":"application/json","X-LFV-Request-Id":K,"X-LFV-Key-Id":Q,"X-LFV-Signature":Z}})}async function t1(X,$,Q=1000){let K=Date.now()+Q;while(Date.now()<K){if(X())return;await new Promise((Y)=>setTimeout(Y,5))}z(X(),$)}function R1(X){let $=new Map;for(let[G,W]of Object.entries(X))$.set(G,{data:W,metadata:{path:G,version:1,created:new Date,contentType:i5.toRegistryEntry()}});let Q=new Map,K=new Set,Y=(G)=>{let W=$.get(G);if(!W)throw Error(`not found: ${G}`);return W};return{source:{type:"vault",isReady:()=>!0,ready:async()=>{},isAvailable:()=>!0,get:()=>{throw Error("not implemented")},getFullSecret:async(G)=>Y(G),getSecret:async(G)=>Y(G).data,putSecret:async(G,W)=>{let H=$.get(G);$.set(G,{data:W,metadata:{path:G,version:(H?.metadata.version??0)+1,created:new Date,contentType:i5.toRegistryEntry()}})},deleteSecret:async(G)=>{$.delete(G)},listPaths:async(G)=>Array.from($.keys()).filter((W)=>W.startsWith(G)),exists:async(G)=>$.has(G),requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async()=>{throw Error("parkKmsKey is not implemented for this test source")},addKmsKeys:async()=>{throw Error("addKmsKeys is not implemented for this test source")},revokeKmsKey:async()=>{throw Error("revokeKmsKey is not implemented for this test source")},getMetadata:async(G)=>Y(G).metadata,subscribe:async(G,W)=>{let H=Q.get(G)??new Set,J=W;return H.add(J),Q.set(G,H),J(Y(G)),()=>{H.delete(J)}},subscribeEvents:(G)=>{return K.add(G),()=>K.delete(G)}},values:$,emit(G,W,H){let J={data:W,metadata:{path:G,version:H,created:new Date,contentType:i5.toRegistryEntry()}};$.set(G,J);for(let j of Q.get(G)??[])j(J)},emitEvent(G){for(let W of K)W(G)}}}function o5(X){return{async load(){return X},async subscribe(){return},secret:{}}}function rQ(X){let $=X,Q;return{async load(){return $},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){$=K,Q?.(K)},secret:{}}}function a5(X){return{LFV_NAMED:{type:"vault",lfv:{lfvServerUrl:z0.lfvServerUrl,clientId:z0.clientId,path:X,deliveryEndpoint:z0.deliveryEndpoint,eventsEndpoint:z0.eventsEndpoint}}}}function l2(){return{tests:[{name:"RemoteConfig LFV secrets without a callback transport reject apply unless singleProcess is declared",fn:async()=>{let X={id:"ecv-lfv-transport-gate",secrets:a5("/ecv/coordination/apply-reject")},$=p2(o5(X),{validators:g2()}),Q=!1,K="";try{await $.ready(2000)}catch(Y){Q=!0,K=Y instanceof Error?Y.message:String(Y)}$.close?.(),z(Q,"LFV secrets without a callback transport must reject ion.ready()"),z(K.includes("callbackTransport")&&K.includes("singleProcess"),`apply rejection should name callbackTransport and the singleProcess opt-out (got: ${K})`)}},{name:"RemoteConfig LFV secrets apply when singleProcess is declared or a callback transport is wired",fn:async()=>{let X={id:"ecv-lfv-transport-ok",secrets:a5("/ecv/coordination/apply-ok")},$=p2(o5(X),{validators:g2(),lfv:{singleProcess:!0}});await $.ready(2000),z($.isReady(),"singleProcess declaration must allow LFV apply without a transport"),$.close?.();let Q=p2(o5(X),{validators:g2(),lfv:{callbackTransport:new u2}});await Q.ready(2000),z(Q.isReady(),"a wired callback transport must allow LFV apply"),Q.close?.()}},{name:"a live RemoteConfig that introduces LFV without a transport keeps last-known-good",fn:async()=>{let X=rQ({id:"ecv-lfv-transport-lkg",secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),$=p2(X,{validators:g2()});await $.ready(2000),z($.isReady(),"initial HTTP vault secrets apply without a callback transport"),z($.secrets?.listSecretsSources().includes("primary")===!0,"last-known-good keeps the HTTP vault source"),X.push({id:"ecv-lfv-transport-lkg",secrets:a5("/ecv/coordination/apply-lkg")}),await new Promise((K)=>setTimeout(K,30)),z($.isReady(),"the instance stays ready after a rejected LFV live push"),z($.secrets?.listSecretsSources().includes("primary")===!0,"last-known-good HTTP vault source is retained"),z($.secrets?.listSecretsSources().includes("LFV_NAMED")!==!0,"the untransportable LFV source is not applied");let Q=$.configStatus.lastError?.message??"";z(Q.includes("callbackTransport"),`configStatus.lastError should name the missing callback transport (got: ${Q})`),$.close?.()}},{name:"distributed LFV callbacks resolve an OTP waiter on a different replica",fn:async()=>{let X=new u2,{privateJwk:$,publicJwks:Q}=await i1("ecv-coordination-kid"),K={...z0,verifyJwks:f2(Q),deliveryTimeout:1000},Y=o1(K,X),Z=o1(K,X),G=globalThis.fetch;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp")){let D=await a1(z0.deliveryEndpoint,{type:"otp",request_id:j.request_id,otp:`otp-${j.request_id}`,action:j.action,path:j.path},$,"ecv-coordination-kid");return w((await Z.handleDelivery(D)).status,200,"ingress replica accepts signed OTP"),Response.json({status:"pending"},{status:202})}if(J.endsWith("/secrets/request"))return J4({request_id:j.request_id,status:"completed",path:j.path,data:{data:{token:"coordinated"},metadata:{path:j.path,version:1}}},$,"ecv-coordination-kid");throw Error(`Unexpected LFV request: ${J}`)};try{let W=await Y.getSecret(z0.path);w(W.token,"coordinated","requesting replica receives callback through transport")}finally{globalThis.fetch=G,Y.close?.(),Z.close?.()}}},{name:"mirrored LFV self-redeems on the callback replica without a transport or waiter",fn:async()=>{let{privateJwk:X,publicJwks:$}=await i1("ecv-self-redeem-kid"),Q=o1({...z0,verifyJwks:f2($),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),K=globalThis.fetch,Y=0,Z,G=await Q.subscribePassive(z0.path,(W)=>{Z=W});globalThis.fetch=async(W,H)=>{let J=JSON.parse(String(H?.body));return Y+=1,J4({request_id:J.request_id,status:"completed",path:J.path,data:{data:{token:"self-redeemed"},metadata:{path:J.path,version:1}}},X,"ecv-self-redeem-kid")};try{let W=crypto.randomUUID(),H=await Q.handleDelivery(await a1(z0.deliveryEndpoint,{type:"otp",request_id:W,otp:`otp-${W}`,action:"read_secret",path:z0.path},X,"ecv-self-redeem-kid"));w(H.status,200,"the callback replica accepts and redeems the OTP"),w(Y,1,"the callback replica redeems exactly once without a mailbox"),w(Z?.data.token,"self-redeemed","the pulled value is published to the mirror subscriber")}finally{G(),globalThis.fetch=K,Q.close?.()}}},{name:"already-used self-redeem callback is terminally accepted and its replay claim survives",fn:async()=>{let{privateJwk:X,publicJwks:$}=await i1("ecv-used-otp-kid"),Q=o1({...z0,verifyJwks:f2($),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),K=globalThis.fetch;globalThis.fetch=async()=>{return Response.json({error:"otp_already_used"},{status:409})};let Y=crypto.randomUUID(),Z={type:"otp",request_id:Y,otp:`otp-${Y}`,action:"read_secret",path:z0.path,issued_at:Date.now()};try{let G=await Q.handleDelivery(await a1(z0.deliveryEndpoint,Z,X,"ecv-used-otp-kid"));w(G.status,200,"already-used OTP is a terminal accepted callback, not a retryable 503");let W=await Q.handleDelivery(await a1(z0.deliveryEndpoint,Z,X,"ecv-used-otp-kid"));w(W.status,401,"the terminal callback keeps its local replay claim")}finally{globalThis.fetch=K,Q.close?.()}}},{name:"distributed envelopes are re-verified and do not cross a JWKS trust boundary",fn:async()=>{let X=new u2,$=await i1("trusted-kid"),Q=await i1("untrusted-kid"),K=o1({...z0,verifyJwks:f2($.publicJwks),deliveryTimeout:80},X),Y=globalThis.fetch,Z="";globalThis.fetch=async(G,W)=>{return Z=JSON.parse(String(W?.body)).request_id??"",Response.json({status:"pending"},{status:202})};try{let G=K.getSecret(z0.path,{timeout:80});await t1(()=>Z.length>0,"source must issue an OTP request");let W=Z,H=await a1(z0.deliveryEndpoint,{type:"otp",request_id:W,otp:"attacker-otp",action:"read_secret",path:z0.path},Q.privateJwk,"untrusted-kid"),J=await H.text();await X.put(W,{kind:"delivery",key:K.callbackKey,rawBody:J,headers:{requestId:W,keyId:"untrusted-kid",signature:H.headers.get("X-LFV-Signature")??""}},1000);let j=!1;try{await G}catch{j=!0}z(j,"untrusted distributed OTP must not resolve the waiter")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"OEM mirror converges values and removes revoked material",fn:async()=>{let X=R1({"payments/api":{token:"v1"}}),$=R1({}),Q=aQ({from:X.source,to:$.source,paths:["payments/api"],mapPath:(K)=>`tenants/customer-a/${K}`});try{await Q.ready(1000);let K="tenants/customer-a/payments/api";w(($.values.get(K)?.data).token,"v1","initial value is re-stored"),z(!$.values.has(`${K}/__ionite_mirror`),"mirrors store no provenance sidecar");let Y=$.values.get(K)?.metadata.version;X.emit("payments/api",{token:"v1"},1),await new Promise((Z)=>setTimeout(Z,20)),w($.values.get(K)?.metadata.version,Y,"identical replay does not mint a new destination version"),X.emit("payments/api",{token:"v2"},2),await t1(()=>$.values.get(K)?.data?.token==="v2","rotation must update mirror"),X.emitEvent({path:"payments/api",event:"revoked"}),await t1(()=>!$.values.has(K),"revocation must remove mirrored value")}finally{await Q.close()}}},{name:"mirroredConfig loads only from the durable destination",fn:async()=>{let X=R1({"/authoritative/root":{id:"root-v1",readyz:{enabled:!0}}}),$=R1({}),Q=oQ({from:X.source,fromPath:"/authoritative/root",to:$.source,path:"/mirror/root"});try{await Q.mirror.ready(1000),w((await Q.load()).id,"root-v1","config loads from the mirrored destination"),X.emit("/authoritative/root",{id:"root-v2"},2),await t1(()=>$.values.get("/mirror/root")?.data?.id==="root-v2","upstream changes must converge into the destination")}finally{await Q.secret.close?.()}}},{name:"tenantMirror mirrors and purges one tenant without store-owned hooks",fn:async()=>{let X=R1({"/tenant/acme/config":{id:"acme-config"}}),$=R1({}),Q={id:"acme",companyId:"acme",companyName:"Acme",environmentType:"DEV",status:"completed",tenantUrl:"https://app.example/acme",createdAt:new Date,updatedAt:new Date,configLocator:{type:"vault",vaultPath:"/tenant/acme/config"}},Y=tQ({log:nQ},{store:{get:async(Z)=>Z===Q.id?Q:void 0,list:async()=>iQ(1,[Q],0,void 0),upsert:async()=>Q,delete:async()=>1,getIon:async()=>{return}},to:$.source,path:({id:Z})=>`/mirrors/${Z}/config`,sourceFromLocator:()=>X.source});try{await Y.ensure("acme"),await t1(()=>$.values.get("/mirrors/acme/config")?.data?.id==="acme-config","tenant config must be mirrored"),z(!$.values.has("/mirrors/acme/config/__ionite_mirror"),"mirrors store no provenance sidecar"),await Y.purge("acme"),z(!$.values.has("/mirrors/acme/config"),"purge removes the tenant config")}finally{await Y.close()}}}]}}async function sQ(){let X=performance.now(),$=[];for(let Z of l2().tests)$.push(await N(Z.name,async()=>{await Z.fn();return}));let Q=performance.now()-X,K=$.filter((Z)=>!Z.passed).length,Y=$.filter((Z)=>Z.details?.skipped).length;return{suite:"Vault coordination",passed:K===0,tests:$,duration:Q,summary:{total:$.length,passed:$.length-K-Y,failed:K,skipped:Y}}}import{closeAllVaultSockets as w4,setVaultWebSocketCommandTimeout as D4,vault as A0,vaultSocketDiagnostics as d2}from"@ionite/server";import{WebSocketServer as eQ}from"ws";var $7={"tok-a":"CLT-A","tok-b":"CLT-B","tok-solo":"CLT-SOLO"};function Q7(X){return typeof X==="string"?$7[X]:void 0}function c2(X,$){return`${X}::${$}`}function K7(X){let $=X["x-vault-token"],Q=Array.isArray($)?$[0]:$;if(Q)return Q;let K=X.authorization,Y=Array.isArray(K)?K[0]:K;if(Y)return Y.replace(/^Bearer\s+/i,"");return}async function Y7(){let X=new Map,$=new Set,Q=new Set,K=new Set,Y=!1,Z=new Map,G=0,W=new eQ({port:0,verifyClient:(B,q)=>{let L=K7(B.req.headers);if(L&&K.has(L)){q(!1,401,"revoked credential");return}q(!0)}});await new Promise((B)=>W.once("listening",B));let H=W.address();if(typeof H==="string"||!H)throw Error("failed to bind test vault ws server");let J=`ws://127.0.0.1:${H.port}`,j=(B,q)=>{if(B.readyState===B.OPEN)B.send(JSON.stringify(q))},D=(B,q,L,O)=>{let M=X.get(c2(B,q));if(!M)return;return{type:"secret",subscription_id:L,client_id:B,event:O,path:q,subscription:q,data:M.data,metadata:Y?{path:q,created_time:new Date().toISOString()}:{path:q,version:M.version,created_time:new Date().toISOString()}}};return W.on("connection",(B)=>{G+=1,Q.add(B),Z.set(B,new Map),B.on("message",(q)=>{let L;try{L=JSON.parse(q.toString())}catch{return}let O=L.type,M=Q7(L.token);if(O==="subscribe"){let F=String(L.subscription_id??""),T=Array.isArray(L.paths)?String(L.paths[0]):"";if(!M){j(B,{type:"error",subscription_id:F,error:"invalid token"});return}Z.get(B)?.set(F,{clientId:M,path:T});let x=D(M,T,F,"subscribed");if(x)j(B,x);return}if(O==="unsubscribe"){Z.get(B)?.delete(String(L.subscription_id??""));return}if(O==="command"){let F=String(L.id??""),T=String(L.path??"");if($.has(T))return;if(!M){j(B,{type:"command_result",id:F,ok:!1,error:"invalid token",code:"auth",status:403});return}if(L.command==="read_secret"){let x=X.get(c2(M,T));if(!x){j(B,{type:"command_result",id:F,ok:!1,error:"not found",code:"not_found",status:404});return}j(B,{type:"command_result",id:F,ok:!0,result:{path:T,data:x.data,metadata:Y?{path:T,created_time:new Date().toISOString()}:{path:T,version:x.version,created_time:new Date().toISOString()}}});return}j(B,{type:"command_result",id:F,ok:!0,result:{}});return}}),B.on("close",()=>{Q.delete(B),Z.delete(B)}),j(B,{type:"connected"})}),{url:J,connectionCount:()=>G,openCount:()=>Q.size,setSecret:(B,q,L)=>{let O=c2(B,q),M=(X.get(O)?.version??0)+1;X.set(O,{data:L,version:M})},pushUpdate:(B,q,L)=>{let O=c2(B,q),M=(X.get(O)?.version??0)+1;X.set(O,{data:L,version:M});for(let[F,T]of Z)for(let[x,b]of T){if(b.clientId!==B||b.path!==q)continue;let p=D(B,q,x,"updated");if(p)j(F,p)}},dropSockets:()=>{for(let B of Array.from(Q))B.terminate()},ignore:$,setOmitVersions:(B)=>{Y=B},revokeToken:(B)=>{K.add(B)},close:()=>new Promise((B)=>{for(let O of W.clients)O.terminate();let q=!1,L=()=>{if(q)return;q=!0,B()};W.close(()=>L()),setTimeout(L,250)})}}function B4(X){return new Promise(($)=>setTimeout($,X))}async function m0(X,$=5000,Q=50){let K=Date.now();while(Date.now()-K<$){if(X())return!0;await B4(Q)}return X()}async function a0(X){let $=await Y7();try{await X($)}finally{w4(),D4(30000),await $.close()}}function n2(){return{tests:[{name:"multiple sources on one URL share a single socket and demux by subscription_id",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-A","/shared",{who:"A"}),$.setSecret("CLT-B","/shared",{who:"B"});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-a",clientId:"CLT-A"}}),K=A0({type:"vault",websocket:{url:$.url,token:"tok-b",clientId:"CLT-B"}}),Y=[],Z=[],G=await Q.subscribe("/shared",(j)=>Y.push(j.data)),W=await K.subscribe("/shared",(j)=>Z.push(j.data)),H=d2();w(H.sockets,1,"exactly one socket per URL across both sources"),w(H.subscriptions,2,"two registered subscriptions on the shared socket"),w($.connectionCount(),1,"server accepted exactly one connection"),z(Y.some((j)=>j?.who==="A"),"client A receives its own tenant-scoped secret"),z(Z.some((j)=>j?.who==="B"),"client B receives a different secret on the same path"),G(),W();let J=await m0(()=>d2().sockets===0&&$.openCount()===0);z(J,"socket idle-closes after the last subscription is removed")})}},{name:"per-frame auth rejects an invalid token (fail-fast subscribe throws)",fn:async()=>{await a0(async($)=>{let Q=A0({type:"vault",websocket:{url:$.url,token:"not-valid",clientId:"CLT-A"}}),K=!1;try{await Q.subscribe("/shared",()=>{},{mode:"fail-fast"})}catch{K=!0}z(K,"subscribe with an invalid per-frame token must reject")})}},{name:"live update is delivered and deduplicated by version",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-SOLO","/live",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=[],Y=await Q.subscribe("/live",(Z)=>K.push(Z.data.v));z(await m0(()=>K.includes(1)),"initial value delivered"),$.pushUpdate("CLT-SOLO","/live",{v:2}),z(await m0(()=>K.includes(2)),"updated value delivered"),Y()})}},{name:"reconnect replays active subscriptions after an unexpected socket drop",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-SOLO","/recon",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=[],Y=await Q.subscribe("/recon",(G)=>K.push(G.data.v));z(await m0(()=>K.includes(1)),"initial value delivered before drop"),$.setSecret("CLT-SOLO","/recon",{v:2}),$.dropSockets();let Z=await m0(()=>K.includes(2),8000);z(Z,"subscription replayed and newer secret delivered after reconnect"),Y()})}},{name:"a command with no reply rejects after the per-command timeout",fn:async()=>{await a0(async($)=>{D4(250),$.ignore.add("/never");let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=!1;try{await Q.getFullSecret("/never")}catch{K=!0}z(K,"an unanswered websocket command must reject on timeout")})}},{name:"close() releases subscriptions and idle-closes the shared socket",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-SOLO","/closing",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}});await Q.subscribe("/closing",()=>{}),w(d2().sockets,1,"socket open while subscribed"),await Q.close?.();let K=await m0(()=>d2().sockets===0&&$.openCount()===0);z(K,"close() releases the subscription and the socket idle-closes")})}},{name:"version-less deliveries are never deduplicated (server omits metadata.version)",fn:async()=>{await a0(async($)=>{$.setOmitVersions(!0),$.setSecret("CLT-SOLO","/nov",{n:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=[],Y=await Q.subscribe("/nov",(G)=>K.push(G.data.n));z(await m0(()=>K.includes(1)),"initial version-less value delivered");let Z=K.length;$.pushUpdate("CLT-SOLO","/nov",{n:2}),$.pushUpdate("CLT-SOLO","/nov",{n:2}),z(await m0(()=>K.length>=Z+2),"both version-less updates must be delivered (no version → no dedup)"),z(K.slice(Z,Z+2).every((G)=>G===2),"the delivered version-less updates carry the pushed value"),Y()})}},{name:"a throwing onChange is contained; sibling subscriptions still deliver",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-A","/boom",{v:1}),$.setSecret("CLT-B","/safe",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-a",clientId:"CLT-A"}}),K=A0({type:"vault",websocket:{url:$.url,token:"tok-b",clientId:"CLT-B"}}),Y=[],Z=await Q.subscribe("/boom",()=>{throw Error("intentional onChange failure")}),G=await K.subscribe("/safe",(W)=>Y.push(W.data.v));z(await m0(()=>Y.includes(1)),"sibling receives initial value despite the throwing peer"),$.pushUpdate("CLT-A","/boom",{v:2}),$.pushUpdate("CLT-B","/safe",{v:2}),z(await m0(()=>Y.includes(2)),"sibling still receives updates after the throwing subscription fired"),Z(),G()})}},{name:"reconnect rotates to a second credential when the first is revoked",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-A","/rot-a",{v:1}),$.setSecret("CLT-B","/rot-b",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-a",clientId:"CLT-A"}}),K=A0({type:"vault",websocket:{url:$.url,token:"tok-b",clientId:"CLT-B"}}),Y=[],Z=await Q.subscribe("/rot-a",()=>{}),G=await K.subscribe("/rot-b",(H)=>Y.push(H.data.v));z(await m0(()=>Y.includes(1)),"initial delivery before the credential is revoked"),$.revokeToken("tok-a"),$.setSecret("CLT-B","/rot-b",{v:2}),$.dropSockets();let W=await m0(()=>Y.includes(2),20000);z(W,"reconnect recovered via the second (still-valid) credential and replayed subscriptions"),Z(),G()})}},{name:"wait-mode subscribe rejects when the socket is disposed before the first secret",fn:async()=>{await a0(async($)=>{let K=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}).subscribe("/never-arrives",()=>{},{mode:"wait"}),Y=!1,Z="";K.then(()=>{},(G)=>{Y=!0,Z=G instanceof Error?G.message:String(G)}),await B4(200),w4();try{await K}catch(G){Y=!0,Z=G instanceof Error?G.message:String(G)}z(Y,"wait-mode subscribe must reject when the socket is disposed (no hang)"),z(Z.toLowerCase().includes("disposed")||Z.toLowerCase().includes("interrupted"),`rejection should explain the socket teardown (got: ${Z})`)})}}]}}async function Z7(){let X=performance.now(),{tests:$}=n2(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Vault websocket transport",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}function i2(X,$){return z(typeof X==="string"&&X.length>0,`${$} must be configured`),X}function v1(X,$){return z(typeof X==="object"&&X!==null&&!Array.isArray(X),`${$} must be an object`),X}function P4(X,$){return z(typeof X==="boolean",`${$} must be a boolean`),X}function X7(X,$){return z(Array.isArray(X),`${$} must be an array`),X}function z7(){return{"~standard":{validate:(X)=>{if(typeof X!=="object"||X===null)return{issues:[{message:`Expected object, got ${typeof X}`}]};let $=X,Q=[];if(typeof $.kty!=="string")Q.push({message:"Expected kty to be string",path:["kty"]});if(typeof $.kid!=="string")Q.push({message:"Expected kid to be string",path:["kid"]});if(Q.length>0)return{issues:Q};return{value:X}}}}}function q4(){return{"~standard":{validate:(X)=>{if(typeof X!=="object"||X===null)return{issues:[{message:`Expected object, got ${typeof X}`}]};let $=X,Q=[];if(typeof $.valid!=="boolean")Q.push({message:"Expected valid to be boolean",path:["valid"]});if(Q.length>0)return{issues:Q};return{value:X}}}}}var F4={validatePath:"/api/workload/validate",jwksPath:"/api/workload/jwks"};async function t5(X){if(X.validToken)return X.validToken;return k0(V1(X.ecvUrl,"Workload validation ecvUrl when validToken is absent"))}async function L4(X,$){return N("Workload JWKS Endpoint",async()=>{let Q=await $(i2(X.jwksPath,"jwksPath"));z(Q.status===200,`Expected 200 OK, got ${Q.status}`);let K=v1(await Q.json(),"JWKS response"),Y=X7(K.keys,"JWKS keys"),Z=z7();for(let G of Y)r0(G,Z);return{details:{keyCount:Y.length,algorithms:Y.map((G)=>v1(G,"JWKS key").alg).filter((G)=>typeof G==="string")}}})}async function V4(X,$,Q){return N("Workload Validate Endpoint",async()=>{let K=await $(i2(X.validatePath,"validatePath"),{method:"POST",headers:{Authorization:`Bearer ${Q}`}});z(K.status===200,`Expected 200 OK for valid token, got ${K.status}`);let Y=v1(await K.json(),"Token validation response"),Z=q4();r0(Y,Z);let G=P4(Y.valid,"Validation result valid");z(G===!0,"Token should be valid");let W=new Set(["valid","expiresAt"]),H=Object.keys(Y).filter((J)=>!W.has(J));return z(H.length===0,`Validate response leaked fields: ${H.join(", ")}`),{details:{valid:G,expiresAt:Y.expiresAt}}})}async function N4(X,$){return N("Workload Validate Endpoint (Invalid Token)",async()=>{let Q=await $(i2(X.validatePath,"validatePath"),{method:"POST",headers:{Authorization:"Bearer invalid.token.here"}});z(Q.status===401,`Expected 401 Unauthorized for invalid token, got ${Q.status}`);let K=v1(await Q.json(),"Invalid token validation response"),Y=q4();r0(K,Y);let Z=P4(K.valid,"Validation result valid");z(Z===!1,"Invalid token should not validate"),z(K.error==="invalid_token",`Expected generic 'invalid_token' error, got ${String(K.error)}`);let G=new Set(["valid","error"]),W=Object.keys(K).filter((H)=>!G.has(H));return z(W.length===0,`Invalid-token response leaked fields: ${W.join(", ")}`),{details:{valid:Z,error:K.error}}})}async function O4(X,$){return N("Workload Validate Endpoint (No Auth)",async()=>{let Q=await $(i2(X.validatePath,"validatePath"),{method:"POST"});return z(Q.status===401,`Expected 401 Unauthorized for missing auth, got ${Q.status}`),{details:{status:Q.status,requiresAuth:!0}}})}async function M4(X,$){return N("Workload Authentication (Whoami)",async()=>{let K=`${V1(X.ecvUrl,"Workload validation ecvUrl")}/api/whoami`,Y=await globalThis.fetch(K,{headers:{Authorization:`Bearer ${$}`}});if(Y.status===404)return{details:{skipped:!0,reason:"ECV whoami endpoint not available"}};z(Y.status===200,`Expected 200 OK, got ${Y.status}`);let Z=v1(await Y.json(),"Whoami response"),G=v1(Z.workload,"Whoami workload");return{details:{workloadId:G.workloadId,clientId:G.clientId}}})}async function G7(X){let $=performance.now(),Q={...F4,...X},K=r(Q),Y=[];Y.push(await L4(Q,K)),Y.push(await O4(Q,K)),Y.push(await N4(Q,K));let Z=await t5(Q);Y.push(await V4(Q,K,Z)),Y.push(await M4(Q,Z));let G=performance.now()-$,W=Y.filter((j)=>j.passed).length,H=Y.filter((j)=>!j.passed).length,J=Y.filter((j)=>j.details?.skipped).length;return{suite:"Workload",passed:H===0,tests:Y,duration:G,summary:{total:Y.length,passed:W-J,failed:H,skipped:J}}}function o2(X){let $={...F4,...X},Q=r($);return[{name:"JWKS endpoint returns valid keys",fn:async()=>{let K=await L4($,Q);if(!K.passed)throw Error(K.error)}},{name:"validate endpoint rejects missing auth",fn:async()=>{let K=await O4($,Q);if(!K.passed)throw Error(K.error)}},{name:"validate endpoint rejects invalid tokens",fn:async()=>{let K=await N4($,Q);if(!K.passed)throw Error(K.error)}},{name:"validate endpoint accepts valid token",fn:async()=>{let K=await t5($),Y=await V4($,Q,K);if(!Y.passed)throw Error(Y.error)}},{name:"whoami endpoint works with workload authentication",fn:async()=>{let K=await t5($),Y=await M4($,K);if(!Y.passed&&!Y.details?.skipped)throw Error(Y.error)}}]}var J7="/api/ionite";function j7(X){let $=typeof X.basePath==="function"?X.basePath():X.basePath;return X.capabilitiezPath??`${$??J7}/capabilitiez`}function s5(X){let $=typeof X.ecv==="function"?X.ecv():X.ecv;if(!$)throw Error("ECV server handle is required");return $.baseUrl}function T4(X,$){z(typeof X==="object"&&X!==null,`capabilities.${$} must be an object`);let Q=X;if(z(typeof Q.module==="string",`capabilities.${$}.module must be a string`),z(typeof Q.active==="boolean",`capabilities.${$}.active must be a boolean`),z(typeof Q.ready==="boolean",`capabilities.${$}.ready must be a boolean`),z(typeof Q.testable==="boolean",`capabilities.${$}.testable must be a boolean`),Q.urls!==void 0)z(Array.isArray(Q.urls)&&Q.urls.every((K)=>typeof K==="string"),`capabilities.${$}.urls must be a string array when provided`);if(Q.reason!==void 0)z(typeof Q.reason==="string",`capabilities.${$}.reason must be a string when provided`);return Q}function r5(X,$="manifest"){if(Array.isArray(X)){X.forEach((K,Y)=>r5(K,`${$}[${Y}]`));return}if(typeof X!=="object"||X===null)return;let Q=new Set(["apiKey","clientSecret","privateKey","remoteConfig","signingKey","token"]);for(let[K,Y]of Object.entries(X))z(!Q.has(K),`capabilitiez manifest must not expose sensitive field ${$}.${K}`),r5(Y,`${$}.${K}`)}async function e5(X){let $=r(X),Q=await k0(s5(X),x5),K=await $(j7(X),{headers:{Authorization:`Bearer ${Q}`}});z(K.status===200,`Expected 200 from capabilitiez endpoint, got ${K.status}. Ensure RemoteConfig includes capabilitiez: true and workload auth is configured.`);let Y=await K.json();z(typeof Y.sdk==="object"&&Y.sdk!==null,"capabilitiez manifest must include sdk"),z(typeof Y.sdk.version==="string","capabilitiez manifest sdk.version must be a string"),z(Number.isInteger(Y.sdk.catalogVersion)&&Y.sdk.catalogVersion>0,"capabilitiez manifest sdk.catalogVersion must be a positive integer"),z(typeof Y.basePath==="string","capabilitiez manifest must include basePath:string"),z(typeof Y.lfvCallbackTransport==="boolean","capabilitiez manifest must include lfvCallbackTransport:boolean"),z(typeof Y.urls==="object"&&Y.urls!==null,"capabilitiez manifest must include urls"),z(typeof Y.capabilities==="object"&&Y.capabilities!==null,"capabilitiez manifest must include capabilities");for(let Z of W7)T4(Y.capabilities[Z.key],Z.key);for(let Z of Object.keys(Y.capabilities))T4(Y.capabilities[Z],Z);return r5(Y),Y}function w7(X){return X.toLowerCase().replace(/§\d+(?:\.\d+)*/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function r1(X,$,Q,K,Y=`${X}.${w7(Q)}`){return{id:Y,unit:X,suite:$,name:Q,title:`ECV(${Y}) ${Q}`,run:K}}function a2(X,$){return z(X!==void 0,`${$} requires the capabilitiez manifest`),X}function t2(X){let $;return(Q)=>{return $??=X(Q),$}}function j0(X,$,Q,K){for(let Y of K)X.push(r1($,Q,Y.name,async()=>Y.fn()))}function b0(X,$,Q,K,Y){K.forEach((Z,G)=>{X.push(r1($,Q,Z.name,async(W)=>{let H=Y(a2(W,$))[G];z(H!==void 0,`${$} test "${Z.name}" could not be resolved`),await H.fn()}))})}function D7(X){j0(X,"sdk.config-retry","SDK / Config retry policy",M5()),j0(X,"sdk.config-lifecycle","SDK / Config lifecycle",N5()),j0(X,"sdk.config-boundary","SDK / Config boundary",O5()),j0(X,"sdk.harness","SDK / Harness",_5()),j0(X,"sdk.authz-closure","SDK / Authz closure",J2().tests),j0(X,"sdk.lfv-callbacks","SDK / LFV callbacks",D2().tests),j0(X,"sdk.secrets-hardening","SDK / Secrets hardening",T2().tests),j0(X,"sdk.vault-coordination","SDK / Vault coordination",l2().tests),j0(X,"sdk.vault-websocket","SDK / Vault WebSocket",n2().tests),j0(X,"sdk.iam-delta","SDK / IAM delta",L2().tests),j0(X,"sdk.kms","SDK / KMS",V2().tests),j0(X,"sdk.otel","SDK / OpenTelemetry",O2().tests),j0(X,"sdk.protocol-surface","SDK / Protocol surface",M2().tests),j0(X,"sdk.sso-id-token-verification","SDK / SSO ID token verification",b2().tests),j0(X,"sdk.sso-control-contract","SDK / SSO control contract",_2().tests),j0(X,"sdk.sso-token-client-auth","SDK / SSO token client auth",k2().tests),j0(X,"sdk.sso-session-window","SDK / SSO session window",C2().tests)}function $9(X){return[r1("meta.run","ECV metadata",H$(H7).replace(/^ECV\(meta\.run\)\s*/,""),async($,Q)=>J$(a2($,"meta.run"),X(),Q),"meta.run"),r1("meta.parity","ECV metadata","declared claims match runtime-testable capabilities",async($,Q)=>j$(a2($,"meta.parity"),X(),Q),"meta.parity")]}function B7(X,$,Q={}){let K=[],Y=x2({...X}),Z=t2((G)=>x2({...X,loginPath:G.urls.sso.loginUrl,callbackPath:G.urls.sso.callbackUrl,userPath:G.urls.sso.userUrl,logoutPath:G.urls.sso.logoutUrl,backChannelLogoutPath:G.urls.sso.logoutBackChannelUrl,tokenPath:G.urls.sso.loginUrl.replace(/\/login$/,"/token"),refreshPath:G.urls.sso.refreshUrl}));if($==="sso.login"){if(b0(K,"sso.login","SSO / Core login",Y.tests,(G)=>Z(G).tests),Q.expectedCallbackErrorPattern){let G=Q.expectedCallbackErrorPattern;b0(K,"sso.login","SSO / Core login",Y.ext.createCallbackErrorSurfacedTests(G),(W)=>Z(W).ext.createCallbackErrorSurfacedTests(G))}}if($==="sso.jit")b0(K,"sso.jit","SSO / JIT",Y.ext.createJITTests(),(G)=>Z(G).ext.createJITTests());if($==="sso.logout")b0(K,"sso.logout","SSO / Logout",Y.ext.createLogoutTests(),(G)=>Z(G).ext.createLogoutTests());if($==="sso.back_channel_logout")b0(K,"sso.back_channel_logout","SSO / Back-channel logout",Y.ext.createBackChannelLogoutTests(),(G)=>Z(G).ext.createBackChannelLogoutTests());return K}function P7(X,$){let Q=[],K=w2({...X}),Y=t2((Z)=>w2({...X,customerPath:Z.urls.ciam.customerUrl,magicLinkPath:Z.urls.ciam.magicLinkUrl,magicLinkLoginPath:Z.urls.ciam.magicLinkLoginUrl,logoutPath:Z.urls.ciam.logoutUrl,customerProvidersPath:Z.urls.ciam.customerProvidersUrl,customerLoginPath:Z.urls.ciam.customerLoginUrl}));if($==="ciam.customer")b0(Q,"ciam.customer","CIAM / Customer",K.ext.createCustomerTests(),(Z)=>Y(Z).ext.createCustomerTests());if($==="ciam.magic_link")b0(Q,"ciam.magic_link","CIAM / Magic link",K.ext.createMagicLinkTests(),(Z)=>Y(Z).ext.createMagicLinkTests());if($==="ciam.oidc_login")b0(Q,"ciam.oidc_login","CIAM / OIDC login",K.ext.createOidcLoginTests(),(Z)=>Y(Z).ext.createOidcLoginTests());if($==="ciam.logout")b0(Q,"ciam.logout","CIAM / Logout",K.ext.createLogoutTests(),(Z)=>Y(Z).ext.createLogoutTests());return Q}function q7(X,$){let Q=[],K=()=>s5(X),Y=F2({...X,ecvUrl:K,ion:X.ion}),Z=t2((G)=>F2({...X,ecvUrl:K,scimPath:G.urls.iam.basePath,groupsInboundPath:G.urls.iam.groupsUrl,ion:X.ion}));if($==="iam.users")b0(Q,"iam.users","IAM / Users",Y.tests,(G)=>Z(G).tests);if($==="iam.groups")b0(Q,"iam.groups","IAM / Groups outbound",Y.ext.createGroupsOutboundTests(),(G)=>Z(G).ext.createGroupsOutboundTests()),b0(Q,"iam.groups","IAM / Groups inbound",Y.ext.createGroupsInboundTests(),(G)=>Z(G).ext.createGroupsInboundTests());return Q}function F7(X,$){let Q=[];if($==="workload.incoming"){let K=()=>s5(X),Y=o2({...X,ecvUrl:K}),Z=t2((G)=>o2({...X,ecvUrl:K,validatePath:G.urls.workload.validateUrl,jwksPath:G.urls.workload.jwksUrl}));b0(Q,"workload.incoming","Workload / Incoming",Y,Z)}if($==="workload.outgoing")Q.push(r1("workload.outgoing","Workload / Outgoing","runtime capability is testable",async(K)=>{let Y=a2(K,"workload.outgoing");z(Y.capabilities["workload.outgoing"].testable,Y.capabilities["workload.outgoing"].reason??"workload.outgoing is not testable")}));return Q}function L7(X){let $=[],Q={ecv:X.ecv,timeout:X.timeout},K=j2(Q);return b0($,"authz.oaa","Authz / OAA",K,()=>K),$}function Q9(){let X=[];return D7(X),X}function K9(X,$={}){let Q=[],K=n5({baseUrl:$.baseUrl??X.baseUrl,timeout:X.timeout,skipTlsVerify:X.skipTlsVerify,tenantPath:$.tenantPath,profile:$.profile,webhookPath:$.webhookPath,ecv:()=>{let Y=typeof X.ecv==="function"?X.ecv():X.ecv;return z(Y!==void 0,"ecv.tenants() requires the ECV server handle returned by setup()"),Y}});return j0(Q,"tenant.management","Tenant / Management",K),Q}function Y9(X,$,Q={}){if($.startsWith("sso."))return B7(X,$,Q);if($.startsWith("ciam."))return P7(X,$);if($.startsWith("iam."))return q7(X,$);if($.startsWith("workload."))return F7(X,$);return L7(X)}import{GenericContainer as _4,Wait as V7}from"testcontainers";async function r2(X,$,Q){let K=await fetch(`${X.baseUrl}/__ecv${$}`,{...Q,headers:{Authorization:`Bearer ${X.env.ION_VAULT_TOKEN}`,...Q?.body?{"Content-Type":"application/json"}:{},...Q?.headers}});if(!K.ok){let Y=await K.text();throw Error(`ECV control API ${Q?.method??"GET"} ${$} failed: ${K.status} ${Y}`)}if(K.status===204)return;return await K.json()}function x4(X){return{...X,reset:()=>r2(X,"/reset",{method:"POST"}),setTestUser:($)=>r2(X,"/test-user",{method:"PUT",body:JSON.stringify($)}),oaa:{...X.oaa,pushes:()=>r2(X,"/oaa/pushes"),clear:()=>r2(X,"/oaa/pushes",{method:"DELETE"})}}}async function Z9(X,$="local-ecv-token"){let Q=X.replace(/\/$/,""),K=await fetch(`${Q}/__ecv/info`,{headers:{Authorization:`Bearer ${$}`}});if(!K.ok){let G=await K.text();throw Error(`ECV control API GET /info failed: ${K.status} ${G}`)}let Y=await K.json(),Z=new URL(Q);return x4({...Y,baseUrl:Q,host:Z.hostname,port:Number(Z.port||(Z.protocol==="https:"?443:80))})}var s2=3515;class X9{container;ecv;constructor(X,$){this.container=X,this.ecv=$}get handle(){return this.ecv}serialize(){let{reset:X,setTestUser:$,oaa:Q,...K}=this.ecv,{pushes:Y,clear:Z,...G}=Q;return{...K,oaa:G}}async stop(){await this.container.stop()}}class e2{options;constructor(X={}){this.options=X}async createContainer(){if(this.options.dockerfileContext)return _4.fromDockerfile(this.options.dockerfileContext,this.options.dockerfileName??"packages/ecv-server/Dockerfile").build(this.options.image??"ionite/ecv:local",{deleteOnExit:!0});return new _4(this.options.image??process.env.ECV_IMAGE??`quay.io/ionite/ecv:${Q1}`)}async start(){let X=await this.createContainer();X.withExposedPorts(s2).withEnvironment({HOST:"0.0.0.0",PORT:String(s2),...this.options.redisUrl?{REDIS_URL:this.options.redisUrl}:{}}).withWaitStrategy(V7.forHttp("/readyz",s2)).withStartupTimeout(this.options.startupTimeout??120000);let $=await X.start(),Q=`http://${$.getHost()}:${$.getMappedPort(s2)}`,K=await Z9(Q);return new X9($,K)}}import{GenericContainer as R4,Wait as v4}from"testcontainers";async function N7(X={}){let $,Q,K;try{let Y=X.redisUrl;if(!Y)$=await new R4(X.redisImage??"redis:7-alpine").withExposedPorts(6379).withWaitStrategy(v4.forLogMessage(/Ready to accept connections/)).start(),Y=`redis://${$.getHost()}:${$.getMappedPort(6379)}`;let Z=X.postgres?.url;if(X.postgres&&!Z){let G=X.postgres;Q=await new R4(G.image??"postgres:16-alpine").withEnvironment({POSTGRES_DB:G.database,POSTGRES_USER:G.user,POSTGRES_PASSWORD:G.password}).withExposedPorts(5432).withWaitStrategy(v4.forLogMessage(/database system is ready to accept connections/)).start(),Z=`postgres://${encodeURIComponent(G.user)}:${encodeURIComponent(G.password)}@${Q.getHost()}:${Q.getMappedPort(5432)}/${encodeURIComponent(G.database)}`}return K=await new e2({...X.ecv,redisUrl:Y}).start(),{ecv:K.serialize(),redisUrl:Y,databaseUrl:Z,async stop(){await Promise.allSettled([K?.stop(),Q?.stop(),$?.stop()].filter(Boolean))}}}catch(Y){throw await Promise.allSettled([K?.stop(),Q?.stop(),$?.stop()].filter(Boolean)),Y}}import*as b1 from"node:crypto";import{defaultLogger as E4}from"@ionite/core";import{CACHE_BOUNDS as G1,clearJwksCache as Q5,resolveCacheConfig as z9,sso as h4,verifyIdToken as U4}from"@ionite/core/server";import{createSessionReadCache as b4,InMemorySessionStore as U1}from"@ionite/server";import{createValidators as C4}from"@ionite/valibot";var K5="https://idp.ecv.local/realms/ecv",Y5="ecv-oidc-client",j9="ion.sso";function G9(X){let{publicKey:$,privateKey:Q}=b1.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function W9(X){return Buffer.from(X).toString("base64url")}function H9(X,$){let Q={alg:"RS256",typ:"JWT",kid:X.kid},K=`${W9(JSON.stringify(Q))}.${W9(JSON.stringify($))}`,Y=b1.createSign("RSA-SHA256");return Y.update(K),`${K}.${W9(Y.sign(X.privateKey))}`}function $2(X={}){let $=Math.floor(Date.now()/1000);return{iss:K5,aud:Y5,sub:"ecv-user",iat:$,exp:$+3600,...X}}function y4(){return`https://jwks.ecv.local/${b1.randomUUID()}`}function s1(X){let $={get:0,update:0,delete:0};return{store:{async create(K){await X.create(K)},async get(K){return $.get++,X.get(K)},async update(K,Y){$.update++,await X.update(K,Y)},async delete(K){$.delete++,await X.delete(K)}},counts:$}}function e1(X){let $=new Date(Date.now()+3600000);return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"opaque-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",expires:new Date(Date.now()+3600000).toISOString()},claims:$2({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:$,createdAt:new Date}}function $5(X){return new Request("https://app.ecv.local/",{headers:{cookie:`${j9}.session=${X}`}})}function J9(X){let $=C4(),Q=h4($.sso,E4,void 0,{sessionStore:X,cookiesPrefix:j9},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!Q)throw Error("sso() returned undefined");return Q}function O7(X,$,Q){let K=C4(),Y=h4(K.sso,E4,void 0,{authority:K5,tokenUrl:$,authorizationUrl:"https://idp.ecv.local/authorize",clientId:Y5,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Q,sessionStore:X,cookiesPrefix:j9,cookiesSecure:!1},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!Y)throw Error("sso() returned undefined");return Y}function M7(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"old-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"old-refresh",expires:new Date(Date.now()-1000).toISOString()},claims:$2({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}async function T7(X,$,Q){let K=globalThis.fetch,Y=0;Q5(X),globalThis.fetch=async(Z,G)=>{if((typeof Z==="string"?Z:Z.toString())===X)return Y++,new Response(JSON.stringify({keys:$()}),{status:200,headers:{"Content-Type":"application/json"}});return K(Z,G)};try{return await Q(()=>Y)}finally{globalThis.fetch=K,Q5(X)}}function a(X,$){if(!X)throw Error($)}function t0(X,$,Q){if(X!==$)throw Error(`${Q} (expected ${$}, got ${X})`)}var x7=(X)=>new Promise(($)=>setTimeout($,X));function A4(){return{tests:[{name:"SSO read path builds user from stored claims with NO JWKS fetch",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-read"));let K=J9($),Y=globalThis.fetch,Z=0;globalThis.fetch=async(G,W)=>{return Z++,Y(G,W)};try{let G=await K.resolveUser($5("sid-read"));a(G.status==="authenticated",`expected authenticated, got ${G.status}`),a(G.value.id==="ecv-user",`expected id ecv-user, got ${String(G.value.id)}`),a(Z===0,`read path performed ${Z} fetch(es); expected 0`),t0(Q.get,1,`expected 1 store.get, got ${Q.get}`)}finally{globalThis.fetch=Y}}},{name:"authenticated SSO read path performs ZERO session-store writes (§7.5)",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-nowrite"));let K=J9($);for(let Y=0;Y<3;Y++){let Z=await K.resolveUser($5("sid-nowrite"));a(Z.status==="authenticated",`expected authenticated, got ${Z.status}`)}t0(Q.update,0,`read path must not write (got ${Q.update} update(s))`),t0(Q.delete,0,`read path must not delete a live session (got ${Q.delete})`)}},{name:"SSO refresh rotates the sid, strips the old refresh_token, and never 503s (§1.2)",fn:async()=>{let $=G9("kid-rotate"),Q=y4(),K="https://idp.ecv.local/token",Y=new U1,Z=[],G={async create(H){Z.push(H.sid),await Y.create(H)},get:(H)=>Y.get(H),update:(H,J)=>Y.update(H,J),delete:(H)=>Y.delete(H)};await G.create(M7("sid-old"));let W=globalThis.fetch;Q5(Q),globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H.toString();if(j===Q)return new Response(JSON.stringify({keys:[$.jwk]}),{status:200,headers:{"Content-Type":"application/json"}});if(j==="https://idp.ecv.local/token"){let D=Math.floor(Date.now()/1000),P=H9($,$2({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com",iat:D}));return new Response(JSON.stringify({access_token:"new-access",id_token:P,token_type:"Bearer",refresh_token:"new-refresh",expires_in:3600,refresh_expires_in:86400,scope:"openid"}),{status:200,headers:{"Content-Type":"application/json"}})}return W(H,J)};try{let J=await O7(G,"https://idp.ecv.local/token",Q).resolveUser($5("sid-old"));a(J.status==="authenticated",`an expired token with a valid refresh token must refresh, never 503 — got ${J.status}`);let j=Z.find((q)=>q!=="sid-old");a(!!j,"refresh must rotate to a new sid (new record created)");let D=await G.get(j);a(D?.tokens.access_token==="new-access","the rotated record holds the refreshed access token"),a(D?.tokens.refresh_token==="new-refresh","the rotated record holds the new refresh token");let P=await G.get("sid-old");a(P!=null,"old record survives during the rotation grace window"),a(P?.tokens.refresh_token===void 0,"old record must have its refresh_token stripped during grace");let B=new Date(P?.expiresAt).getTime();a(B<=Date.now()+60000+2000,`old record window must shrink to the grace (<=~60s), got ${B-Date.now()}ms`)}finally{globalThis.fetch=W,Q5(Q)}}},{name:"resolveUser is memoized per Request object (single store round-trip)",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-memo"));let K=J9($),Y=$5("sid-memo");await Promise.all([K.resolveUser(Y),K.resolveUser(Y),K.resolveUser(Y)]),t0(Q.get,1,`same-request resolveUser should hit the store once, got ${Q.get}`)}},{name:"session read cache: repeat get is served locally; miss is not cached; update/delete invalidate",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-cache"));let K=b4($,{ttlMs:30000,maxEntries:1024});await K.get("sid-cache"),await K.get("sid-cache"),t0(Q.get,1,`second get within TTL should be local, underlying got ${Q.get}`),await K.get("missing"),await K.get("missing"),t0(Q.get,3,`misses must not be cached (expected 3 underlying gets, got ${Q.get})`),await K.update("sid-cache",{expiresAt:new Date(Date.now()+3600000)}),await K.get("sid-cache"),t0(Q.get,4,`get after update should re-read (expected 4, got ${Q.get})`),await K.delete("sid-cache");let Y=await K.get("sid-cache");a(Y===null,"session should be gone after delete"),t0(Q.get,5,`get after delete should re-read (expected 5, got ${Q.get})`)}},{name:"session read cache: entry expires after ttlMs and re-reads",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-ttl"));let K=b4($,{ttlMs:20,maxEntries:1024});await K.get("sid-ttl"),t0(Q.get,1,`first get reaches store, got ${Q.get}`),await x7(35),await K.get("sid-ttl"),t0(Q.get,2,`get after TTL expiry should re-read (expected 2, got ${Q.get})`)}},{name:"RemoteConfig.cache values are clamped, not honored, when out of range",fn:async()=>{let $=z9({jwks:{ttlSeconds:1,maxEntries:1},session:{read:{enabled:!0,ttlSeconds:0,maxEntries:1}}});a($.jwks.ttlMs===G1.jwksTtlSeconds.min*1000,`jwks.ttl should clamp to min, got ${$.jwks.ttlMs}`),a($.jwks.maxEntries===G1.jwksMaxEntries.min,"jwks.maxEntries should clamp to min"),a($.session.read.ttlMs===G1.sessionReadTtlSeconds.min*1000,"session.read.ttl should clamp to min"),a($.session.read.maxEntries===G1.sessionReadMaxEntries.min,"read.maxEntries clamp to min");let Q=z9({jwks:{ttlSeconds:1e7,maxEntries:1e6},session:{read:{enabled:!0,ttlSeconds:1e4,maxEntries:1e7}}});a(Q.jwks.ttlMs===G1.jwksTtlSeconds.max*1000,"jwks.ttl should clamp to max"),a(Q.jwks.maxEntries===G1.jwksMaxEntries.max,"jwks.maxEntries should clamp to max"),a(Q.session.read.ttlMs===G1.sessionReadTtlSeconds.max*1000,"session.read.ttl should clamp to max");let K=z9(void 0);a(K.jwks.ttlMs===G1.jwksTtlSeconds.default*1000,"jwks.ttl default"),a(K.session.read.enabled===!1,"read cache must default to disabled")}},{name:"JWKS/CryptoKey caching: repeated verifications = one fetch; verifies after key rotation",fn:async()=>{let $=G9("kid-perf-1"),Q=G9("kid-perf-2"),K=y4(),Y=[$.jwk];await T7(K,()=>Y,async(Z)=>{for(let W=0;W<3;W++){let H=await U4({idToken:H9($,$2()),jwksUri:K,issuer:K5,audience:Y5});a(H.sub==="ecv-user","repeated verify should resolve claims")}a(Z()===1,`3 verifications should share one JWKS fetch, got ${Z()}`),Y=[$.jwk,Q.jwk];let G=await U4({idToken:H9(Q,$2()),jwksUri:K,issuer:K5,audience:Y5});a(G.sub==="ecv-user","rotated-key token must verify after refresh"),a(Z()===2,`rotation should trigger exactly one refresh, got ${Z()}`)})}}]}}async function _7(){let X=performance.now(),{tests:$}=A4(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Performance tuning",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{isStoreConflictError as w9,silentLogger as c}from"@ionite/core";import{chunkedMget as R7,createEvalshaRunner as v7,createReactiveRedis as S4,DEFAULT_LIST_LIMIT as U7,DEFAULT_MGET_CHUNK_SIZE as b7,encodeKeyId as y7,MAX_LIST_LIMIT as k4,RedisAccessAssignmentStore as E7,RedisAccessCatalogStore as I4,RedisAccessIndex as g0,RedisMagicLinkStore as m4,RedisSessionStore as Q2,RedisUserStore as K2,scriptSha1 as D9,supportsRunScript as Y2}from"@ionite/redis-core";var I="ecv:redis:";function G0(){let X=new Map,$=new Map,Q=new Map,K={get:0,set:0,del:0,sadd:0,srem:0,smembers:0,mget:0,runScript:0},Y={raw:X,sets:$,ttls:Q,mgetCalls:0,commands:K,roundTrips:0,resetCounts(){Y.mgetCalls=0,Y.roundTrips=0;for(let Z of Object.keys(K))K[Z]=0},async get(Z){return K.get+=1,Y.roundTrips+=1,X.has(Z)?X.get(Z):null},async set(Z,G){K.set+=1,Y.roundTrips+=1,X.set(Z,G),Q.delete(Z)},async setex(Z,G,W){Y.roundTrips+=1,X.set(Z,W),Q.set(Z,Date.now()+G*1000)},async del(Z){K.del+=1,Y.roundTrips+=1,X.delete(Z),$.delete(Z),Q.delete(Z)},async ttl(Z){if(!X.has(Z))return-2;let G=Q.get(Z);if(G==null)return-1;return Math.max(0,Math.ceil((G-Date.now())/1000))},async getdel(Z){Y.roundTrips+=1;let G=X.has(Z)?X.get(Z):null;return X.delete(Z),Q.delete(Z),G},async sadd(Z,...G){K.sadd+=1,Y.roundTrips+=1;let W=$.get(Z)??new Set;for(let H of G)W.add(H);$.set(Z,W)},async srem(Z,...G){K.srem+=1,Y.roundTrips+=1;let W=$.get(Z);if(W)for(let H of G)W.delete(H)},async smembers(Z){return K.smembers+=1,Y.roundTrips+=1,Array.from($.get(Z)??[])},async mget(Z){return K.mget+=1,Y.mgetCalls+=1,Y.roundTrips+=1,Z.map((G)=>X.has(G)?X.get(G):null)}};return Y}function Z2(){let X=G0(),$=(W)=>X.sets.get(W)?.size??0,Q=(W,H)=>{let J=X.sets.get(W)??new Set;J.add(H),X.sets.set(W,J)},K=(W,H)=>{X.sets.get(W)?.delete(H)},Y=(W)=>Array.from(X.sets.get(W)??[]),Z=(W)=>{X.raw.set(W,"1")},G=(W)=>{X.raw.delete(W),X.sets.delete(W)};return X.runScript=async(W,H,J)=>{if(X.commands.runScript+=1,X.roundTrips+=1,W.includes("local C=tonumber(ARGV[2])")){let j=J[0],D=Number(J[1]),P=Number(J[2]),B=Number(J[3]),q=H[0],L=H[1];for(let F=0;F<D;F+=1){let T=2+F*4;G(H[T]),G(H[T+1]),G(H[T+2]),G(H[T+3])}let O=2+D*4;for(let F=0;F<P;F+=1)G(H[O+F]);G(q);let M=O+P;for(let F=0;F<B;F+=1){let T=M+F*5,[x,b,p,F0,k]=[H[T],H[T+1],H[T+2],H[T+3],H[T+4]],C=4+F*3,[X0,u0,J5]=[J[C],J[C+1],J[C+2]];Q(b,X0),Z(p),Q(F0,J5),Z(k),Q(x,u0),Q(q,u0)}if(B===0)K(L,j);else Q(L,j);return"ok"}if(W.includes("local A=tonumber(ARGV[2])")){let j=J[0],D=Number(J[1]),P=Number(J[2]),B=H[0],q=H[1];for(let M=0;M<D;M+=1){let F=2+M*5,[T,x,b,p,F0]=[H[F],H[F+1],H[F+2],H[F+3],H[F+4]],k=3+M*3,[C,X0,u0]=[J[k],J[k+1],J[k+2]];Q(T,j),Z(x),Q(b,X0),Z(p),Q(B,C),Q(F0,C),Q(q,u0)}let L=2+D*5,O=3+D*3;for(let M=0;M<P;M+=1){let F=L+M*5,[T,x,b,p,F0]=[H[F],H[F+1],H[F+2],H[F+3],H[F+4]],k=O+M*2,[C,X0]=[J[k],J[k+1]];if(K(T,j),$(T)===0){if(G(T),G(x),K(F0,C),K(b,X0),$(b)===0)G(b),G(p)}K(B,C)}return"ok"}if(W.includes("local n=tonumber(ARGV[2])")){let j=J[0],D=Number(J[1]),P=H[0];for(let B=0;B<D;B+=1){let q=1+B*5,[L,O,M,F,T]=[H[q],H[q+1],H[q+2],H[q+3],H[q+4]],[x,b]=[J[2+B*2],J[3+B*2]];if(K(L,j),$(L)===0){if(G(L),G(O),K(T,x),K(M,b),$(M)===0)G(M),G(F)}}return G(P),"ok"}if(W.includes("local roots={}")){let j=Number(J[0]),D=H[0],P=[],B=new Set;for(let q=0;q<j;q+=1){let L=1+q*4,[O,M,F,T]=[H[L],H[L+1],H[L+2],H[L+3]];for(let x of Y(O))if(!B.has(x))B.add(x),P.push(x);G(O),G(M),G(F),G(T)}return G(D),P}if(W.includes("for i=1,#KEYS do")){for(let j of H)G(j);return"ok"}throw Error("authz scripted adapter: unsupported script")},X}function X2(){let X=G0();return X.runScript=async($,Q,K)=>{if($.includes("return 'created'")){let Y=Q[0],Z=K[0],G=X.raw.has(Y)?X.raw.get(Y):null;if(G!=null)return G===Z?"exists_identical":"conflict";if(K.length>=2)await X.setex(Y,Number(K[1]),Z);else await X.set(Y,Z);return"created"}if($.includes("return 'retry'")){let Y=Q[0],Z=K[0],G=K[1],W=X.raw.has(Y)?X.raw.get(Y):null;if(W==null)return"missing";if(W!==Z)return"retry";if(K.length>=3)await X.setex(Y,Number(K[2]),G);else{let H=X.ttls.get(Y);if(X.raw.set(Y,G),H!=null)X.ttls.set(Y,H)}return"ok"}if($.includes("return {1,ARGV[1]}")){let[Y,Z]=Q,[G,W]=K,H=X.raw.get(Y);if(H!==void 0)return[0,H];X.raw.set(Y,G);let J=X.sets.get(Z)??new Set;return J.add(W),X.sets.set(Z,J),[1,G]}throw Error("scripted memory adapter: unsupported script")},X}function y1(X,$,Q){return{sid:X,sub:`user-${$}`,user:{id:`user-${$}`},createdAt:new Date(0),...Q?{expiresAt:Q}:{}}}function f4(){return{tests:[{name:"corrupt JSON payloads are discarded (get -> undefined, list skips) instead of throwing (§1.27)",fn:async()=>{let $=G0(),Q=new K2({redis:$,keyPrefix:I,logger:c});await Q.upsert({id:"u-ok",userName:"ok",name:"OK",email:"ok@example.test"}),$.raw.set(`${I}user:${y7("u-bad")}`,"{ this is : not valid json");let K=$.sets.get(`${I}user:ids`);z(K!=null,"user id set should exist after the first upsert"),K?.add("u-bad");let Y=await Q.get("u-bad");w(Y,void 0,"a corrupt record must read back as undefined, not throw");let Z=await Q.get("u-ok");z(Z?.id==="u-ok","a valid record beside a corrupt one must still read");let G=await Q.list();w(G.items.length,1,"list must skip the corrupt record and return only valid ones"),w(G.items[0]?.id,"u-ok","the surviving record should be the valid one")}},{name:"RedisUserStore maintains the optional externalId secondary index",fn:async()=>{let $=new K2({redis:G0(),keyPrefix:I,logger:c});await $.upsert({id:"local-user",externalId:"enterprise-user",userName:"indexed@example.test",name:"Indexed User",email:"indexed@example.test"}),w((await $.getByExternalId("enterprise-user"))?.id,"local-user","externalId resolves the app-owned user id"),await $.upsert({id:"local-user",externalId:"enterprise-user-renamed",userName:"indexed@example.test",name:"Indexed User",email:"indexed@example.test"}),w(await $.getByExternalId("enterprise-user"),void 0,"renaming clears the stale index"),w((await $.getByExternalId("enterprise-user-renamed"))?.id,"local-user","renaming installs the new index")}},{name:"list() bounds the page: omitted limit -> default cap, oversized limit -> max cap (§7.3)",fn:async()=>{let $=G0(),Q=new K2({redis:$,keyPrefix:I,logger:c}),K=k4*2+50;for(let G=0;G<K;G+=1)await Q.upsert({id:`u-${G}`,userName:`u-${G}`,name:`User ${G}`,email:`u${G}@example.test`});let Y=await Q.list();w(Y.total,K,"total must reflect every matching row (before pagination)"),w(Y.items.length,U7,"an omitted limit must default-cap the page");let Z=await Q.list({limit:K});w(Z.items.length,k4,"an oversized explicit limit must clamp to the max page"),w(Z.total,K,"total stays the true count even when the page is clamped")}},{name:"chunkedMget batches a large key set (no unbounded MGET) and preserves order + nulls (§7.3)",fn:async()=>{let $=Array.from({length:5000},(Z,G)=>`k${G}`),Q=0,K=0,Y=await R7($,async(Z)=>{return K+=1,Q=Math.max(Q,Z.length),Z.map((G)=>Number(G.slice(1))%2===0?G:null)});z(K>1,"a 5000-key MGET must be split into multiple bounded batches"),z(Q<=b7,`each MGET batch must stay within the chunk size (saw ${Q})`),w(Y.length,$.length,"chunked result length must match input length"),w(Y[0],"k0","order must be preserved across chunks (first even key)"),w(Y[1],null,"missing values must normalize to null (first odd key)"),w(Y[4998],"k4998","last even key preserved"),w(Y[4999],null,"last odd key normalizes to null")}},{name:"create-if-absent: identical replay succeeds; a different value is a typed conflict (§5.9)",fn:async()=>{let $=G0(),Q=new Q2({redis:$,keyPrefix:I,logger:c}),K=y1("s1","a");await Q.create(K),await Q.create(y1("s1","a"));let Y;try{await Q.create(y1("s1","b"))}catch(J){Y=J}z(w9(Y),"a different value at the same sid must throw a typed StoreConflictError");let Z=new m4({redis:$,keyPrefix:I,logger:c}),G={id:"c1",userName:"c1"},W=new Date(Date.now()+60000);await Z.create("tok",G,W);let H;try{await Z.create("tok",{id:"c2",userName:"c2"},W)}catch(J){H=J}z(w9(H),"a magic-link token collision must throw a typed StoreConflictError")}},{name:"unique secondary index: a second id claiming an in-use userName conflicts and writes nothing (§5.9)",fn:async()=>{let $=G0(),Q=new K2({redis:$,keyPrefix:I,logger:c});await Q.upsert({id:"u1",userName:"alice",name:"Alice",email:"a@example.test"}),await Q.upsert({id:"u1",userName:"alice",name:"Alice II",email:"a@example.test"});let K;try{await Q.upsert({id:"u2",userName:"alice",name:"Mallory",email:"m@example.test"})}catch(Y){K=Y}z(w9(K),"claiming an in-use userName must throw a typed conflict"),w(await Q.get("u2"),void 0,"a conflicting upsert must not persist the record (checks-first)"),w((await Q.get("u1"))?.name,"Alice II","the original owner record is untouched by the conflict")}},{name:"session update merges fields (no clobber) and a missing session throws (§5.9)",fn:async()=>{let $=G0(),Q=new Q2({redis:$,keyPrefix:I,logger:c});await Q.create(y1("s1","a"));let K=new Date(Date.now()+1e6);await Q.update("s1",{expiresAt:K,tenantId:"tnt-1"});let Y=await Q.get("s1");w(new Date(Y?.expiresAt).getTime(),K.getTime(),"the patched field is applied"),w(Y?.createdAt?.getTime(),0,"unpatched fields are preserved (shallow merge, no clobber)");let Z=!1;try{await Q.update("missing",{tenantId:"x"})}catch{Z=!0}z(Z,"updating a missing session must throw")}},{name:"session create derives a bounded TTL from expiresAt; a rotation-grace update shrinks it (§1.3)",fn:async()=>{let $=X2(),Q=new Q2({redis:$,keyPrefix:I,logger:c});await Q.create(y1("s-ttl","ttl",new Date(Date.now()+1e6)));let K=Array.from($.raw.keys()).find((W)=>W.includes("session"));z(!!K,"a session key must exist after create");let Y=await $.ttl?.(K)??-1;z(Y>0&&Y<=1000,`create must set a bounded TTL from expiresAt (got ${Y})`),await Q.update("s-ttl",{tenantId:"tnt"});let Z=await $.ttl?.(K)??-1;z(Z>0,`an update without expiresAt must KEEPTTL (got ${Z})`),await Q.update("s-ttl",{expiresAt:new Date(Date.now()+60000)});let G=await $.ttl?.(K)??-1;z(G>0&&G<=60,`grace re-TTL must shrink the key TTL to the grace (got ${G})`)}},{name:"session create with no runScript (fallback path) still applies a TTL via setex (§1.3)",fn:async()=>{let $=G0();await new Q2({redis:$,keyPrefix:I,logger:c}).create(y1("s-ttl-fb","fb",new Date(Date.now()+500000)));let K=Array.from($.raw.keys()).find((Z)=>Z.includes("session"));z(!!K,"a session key must exist after fallback create");let Y=await $.ttl?.(K)??-1;z(Y>0&&Y<=500,`fallback create must setex a bounded TTL (got ${Y})`)}},{name:"hasAll/hasAny evaluate many targets with a single batched MGET (§7.4)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c}),K=[{subject:"u1",subjectType:"user",targetType:"permission",target:"p0"}];await Q.materialize("root1",K);let Y=Array.from({length:50},(Z,G)=>({targetType:"permission",target:`p${G}`,resource:`r${G}`}));$.mgetCalls=0,await Q.hasAll("u1",Y,"user"),w($.mgetCalls,1,"hasAll over 50 targets must issue exactly one batched MGET"),$.mgetCalls=0,await Q.hasAny("u1",Y,"user"),w($.mgetCalls,1,"hasAny over 50 targets must issue exactly one batched MGET")}},{name:"access list total stays accurate after revoke (§7.5)",fn:async()=>{let $=G0(),Q=new E7({redis:$,keyPrefix:I,logger:c});await Q.grant({subject:"u1",subjectType:"user",targetType:"role",target:"r1"}),await Q.grant({subject:"u1",subjectType:"user",targetType:"role",target:"r2"});let K=await Q.list();w(K.total,2,"both active grants are counted"),await Q.revoke({subject:"u1",subjectType:"user",targetType:"role",target:"r1"});let Y=await Q.list();w(Y.total,1,"revoke removes the row from the id set so the total is accurate"),w(Y.items.length,1,"only the surviving grant is returned"),w(Y.items[0]?.target,"r2","the surviving grant is the non-revoked one")}},{name:"list() returns a stable deterministic order with disjoint, complete offset pages",fn:async()=>{let $=G0(),Q=new K2({redis:$,keyPrefix:I,logger:c}),K=["u3","u1","u5","u2","u4"];for(let J of K)await Q.upsert({id:J,userName:`${J}@example.com`});let Y=await Q.list({start:0,limit:2}),Z=await Q.list({start:2,limit:2}),G=await Q.list({start:4,limit:2});w(Y.total,5,"total reflects the full set on every page");let W=[...Y.items,...Z.items,...G.items].map((J)=>J.id);w(W.join(","),"u1,u2,u3,u4,u5","pages are disjoint, complete, and in stable ascending id order");let H=await Q.list({start:0,limit:2});w(H.items.map((J)=>J.id).join(","),"u1,u2","the same offset window is byte-stable across repeated list() calls")}},{name:"reactive adapter close() before first readiness rejects ready() instead of hanging (§5.8)",fn:async()=>{let Q=S4({source:{async subscribe(){return()=>{}}},path:"redis/main",createClient:async()=>({adapter:G0()})});await Q.close();let K=!1;try{await Q.ready(50)}catch{K=!0}z(K,"ready() must reject promptly after close() before first readiness, not hang")}},{name:"reactive adapter forwards runScript + getdel from the active client (§10.1)",fn:async()=>{let $=X2(),K=S4({source:{async subscribe(Y,Z){return Z({value:{},metadata:{version:1}}),()=>{}}},path:"redis/main",createClient:async()=>({adapter:$})});await K.ready(1000),z(Y2(K),"reactive adapter must forward runScript once the client supports it"),z(typeof K.getdel==="function","reactive adapter must forward getdel once the client supports it"),await K.set(`${I}fwd:k`,"v"),w(await K.getdel?.(`${I}fwd:k`),"v","forwarded getdel returns and deletes the value"),w(await K.get(`${I}fwd:k`),null,"forwarded getdel deleted the key"),await K.close()}},{name:"single-use consume over runScript/getdel yields exactly one winner (§10.1)",fn:async()=>{let $=X2();z(Y2($),"scripted adapter must advertise runScript for this test");let Q=new m4({redis:$,keyPrefix:I,logger:c}),K={id:"c1",userName:"c1"};await Q.create("one-time",K,new Date(Date.now()+60000));let Z=(await Promise.all(Array.from({length:8},()=>Q.getAndDelete("one-time")))).filter((G)=>G!=null);w(Z.length,1,"exactly one concurrent consume may win the single-use token")}},{name:"session update over the runScript CAS path preserves arrays and large ints (§8.2)",fn:async()=>{let $=X2();z(Y2($),"scripted adapter must advertise runScript for this test");let Q=new Q2({redis:$,keyPrefix:I,logger:c}),K=Number.MAX_SAFE_INTEGER,Y={sid:"s-merge",sub:"user-merge",user:{id:"user-merge"},claims:{groups:[],seq:K},createdAt:new Date(0),expiresAt:new Date(Date.now()+1e6)};await Q.create(Y),await Q.update("s-merge",{tenantId:"tnt-merge"});let Z=await Q.get("s-merge");z(Z!=null,"session must exist after update"),w(Z?.tenantId,"tnt-merge","the patched field is applied"),z(Array.isArray(Z?.claims.groups),"an empty-array claim must stay an array (not become {})"),w((Z?.claims.groups).length,0,"the empty-array claim stays empty"),w(Z?.claims.seq,K,"a large-integer claim survives the merge unchanged")}},{name:"key encoding: injected `:` cannot alias another edge in the access index (§10.2)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-a",[{subject:"u",subjectType:"user",targetType:"permission",target:"p",resource:"x:y"}]),z((await Q.has("u","permission","p","x:y","user")).allow,"the materialized edge (target p, resource x:y) must be allowed"),z(!(await Q.has("u","permission","p:x","y","user")).allow,"a colon-shifted edge must NOT alias the real edge — it must be denied")}},{name:"key encoding: subject-disabled marker is keyed to the exact id, not a `:`-prefix (§10.2)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.setSubjectDisabled("team:a","user",!0),z(await Q.isSubjectDisabled("team:a","user"),"the disabled marker round-trips for a `:`-bearing id"),z(!await Q.isSubjectDisabled("team","user"),"a prefix subject id must not alias another subject’s disabled marker"),z(!await Q.isSubjectDisabled("team:a","customer"),"the veto is namespaced by subjectType")}},{name:"RedisAccessCatalogStore insertIfAbsent is atomic and concurrent callers converge on one row",fn:async()=>{let $=X2(),Q=new I4({redis:$,keyPrefix:I,logger:c}),K=Q.groups.insertIfAbsent({id:"code-admin",name:"Code Admin",description:"Code presentation",source:"code",roles:["admin"]}),Y=Q.groups.insertIfAbsent({id:"code-admin",name:"IAM Renamed Admin",description:"IAM-owned presentation",source:"code",providerId:"provider-42",requestUrl:"https://iam.example.test/request/provider-42",roles:["must-not-win"]});await Promise.all([K.ready(),Y.ready()]);let Z=await Q.groups.get("code-admin");z(Z!==void 0,"one durable catalog row is installed"),w(K.name,Z?.name,"the first caller receives the winning row"),w(Y.name,Z?.name,"the racing caller converges on the same winning row"),w((await Q.groups.list()).items.length,1,"concurrent insertIfAbsent calls never duplicate the id-set row")}},{name:"RedisAccessCatalogStore suppresses reactive emission for a value-identical set",fn:async()=>{let $=new I4({redis:G0(),keyPrefix:I,logger:c}),Q=0,K=$.afterChange?.(()=>{Q+=1});await $.groups.set({id:"stable-group",name:"Stable Group",description:"No-op write proof",requestable:!0,metadata:{first:1,second:2}}).ready(),await $.groups.set({requestable:!0,description:"No-op write proof",name:"Stable Group",id:"stable-group",metadata:{second:2,first:1}}).ready(),K?.(),w(Q,1,"the durable no-op write emits no second reactive change")}},{name:"key encoding: a pathologically long id produces a bounded key (§10.2)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c}),K="x".repeat(1e4);await Q.materialize("root-huge",[{subject:K,subjectType:"user",targetType:"permission",target:K}]);for(let Y of $.raw.keys())z(Y.length<512,`key must stay bounded even for a 10k-char id (saw ${Y.length})`),z(!Y.includes(K),"the raw oversized id must never appear verbatim in a key");for(let Y of $.sets.keys())z(Y.length<512,`set key must stay bounded even for a 10k-char id (saw ${Y.length})`)}},{name:"Lua teardown: removeByRoot of a 1000-edge root is O(1) client round trips, not O(edges) (§7.2)",fn:async()=>{let $=Z2();z(Y2($),"scripted authz adapter must advertise runScript for the Lua path");let Q=new g0({redis:$,keyPrefix:I,logger:c}),K=Array.from({length:1000},(Y,Z)=>({subject:"big",subjectType:"user",targetType:"permission",target:`p${Z}`}));await Q.materialize("root-big",K),z((await Q.hasPermission("big","p0",void 0,"user")).allow,"a materialized edge resolves before teardown"),z((await Q.hasPermission("big","p999",void 0,"user")).allow,"the last materialized edge resolves too"),$.resetCounts(),await Q.removeByRoot("root-big"),w($.commands.smembers,1,"teardown reads the root descriptor set with exactly one SMEMBERS"),w($.commands.runScript,1,"teardown applies every edge removal in exactly one runScript"),z($.roundTrips<=3,`a 1000-edge teardown must be O(1) client round trips, not O(edges) (saw ${$.roundTrips})`),z(!(await Q.hasPermission("big","p0",void 0,"user")).allow,"edges are gone after teardown"),z(!(await Q.hasPermission("big","p999",void 0,"user")).allow,"the last edge is gone after teardown")}},{name:"diff-materialize: re-materializing identical edges is a no-op (no write, no §5.3 deny window)",fn:async()=>{let $=Z2(),Q=new g0({redis:$,keyPrefix:I,logger:c}),K=[{subject:"u",subjectType:"user",targetType:"role",target:"a"},{subject:"u",subjectType:"user",targetType:"role",target:"b"}];await Q.materialize("root-x",K),$.resetCounts(),await Q.materialize("root-x",K),w($.commands.runScript,0,"an empty diff must not invoke the write script at all"),z((await Q.hasRole("u","a",void 0,"user")).allow,"edge a stays present across a no-op re-materialize"),z((await Q.hasRole("u","b",void 0,"user")).allow,"edge b stays present across a no-op re-materialize")}},{name:"replaceSubject atomically installs a complete Redis closure in one script",fn:async()=>{let $=Z2(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-old",[{subject:"swap",subjectType:"user",targetType:"permission",target:"old",rootId:"root-old"}]),$.resetCounts(),await Q.replaceSubject("swap","user",[{subject:"swap",subjectType:"user",targetType:"permission",target:"new",rootId:"root-new"},{subject:"swap",subjectType:"user",targetType:"permission",target:"shared",rootId:"root-new"},{subject:"swap",subjectType:"user",targetType:"permission",target:"shared",rootId:"root-second"}]),w($.commands.runScript,1,"the complete closure replacement uses exactly one atomic script"),z(!(await Q.hasPermission("swap","old",void 0,"user")).allow,"the old closure is removed at the swap"),z((await Q.hasPermission("swap","new",void 0,"user")).allow,"the new closure is installed"),z((await Q.hasPermission("swap","shared",void 0,"user")).allow,"overlapping roots preserve their shared edge"),await Q.removeByRoot("root-new"),z((await Q.hasPermission("swap","shared",void 0,"user")).allow,"the second root still justifies the shared edge")}},{name:"replaceSubject fails before mutation when a Redis adapter lacks atomic scripts",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-old",[{subject:"safe",subjectType:"user",targetType:"permission",target:"old",rootId:"root-old"}]);let K=!1;try{await Q.replaceSubject("safe","user",[])}catch{K=!0}z(K,"replaceSubject requires runScript instead of exposing a non-atomic fallback"),z((await Q.hasPermission("safe","old",void 0,"user")).allow,"last-known-good access survives the rejected replace")}},{name:"diff-materialize: removing one edge leaves the unchanged sibling continuously present (§5.3)",fn:async()=>{let $=Z2(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-y",[{subject:"u",subjectType:"user",targetType:"role",target:"keep"},{subject:"u",subjectType:"user",targetType:"role",target:"drop"}]),await Q.materialize("root-y",[{subject:"u",subjectType:"user",targetType:"role",target:"keep"}]),z((await Q.hasRole("u","keep",void 0,"user")).allow,"the unchanged edge survives a sibling removal"),z(!(await Q.hasRole("u","drop",void 0,"user")).allow,"the removed edge is gone")}},{name:"teardown never touches idx:disabled: a deactivated subject stays vetoed after removeByRoot (§7.2)",fn:async()=>{let $=Z2(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.setSubjectDisabled("u","user",!0),await Q.materialize("root-z",[{subject:"u",subjectType:"user",targetType:"role",target:"a"}]),await Q.removeByRoot("root-z"),z(await Q.isSubjectDisabled("u","user"),"the subject-disabled marker is independent of edge materialization and survives teardown")}},{name:"fallback teardown (no runScript): removeByRoot issues O(edges) commands, not ~5×edges",fn:async()=>{let $=G0();z(!Y2($),"this test intends to exercise the no-runScript fallback");let Q=new g0({redis:$,keyPrefix:I,logger:c}),K=Array.from({length:100},(Y,Z)=>({subject:"f",subjectType:"user",targetType:"permission",target:`p${Z}`}));await Q.materialize("root-fb",K),$.resetCounts(),await Q.removeByRoot("root-fb"),z($.roundTrips<=1200,`fallback teardown must be O(edges) with a small constant (saw ${$.roundTrips})`),z($.roundTrips<1e4,`fallback teardown must not be quadratic in edges (saw ${$.roundTrips})`),z(!(await Q.hasPermission("f","p0",void 0,"user")).allow,"fallback teardown removes edges"),z(!(await Q.hasPermission("f","p99",void 0,"user")).allow,"fallback teardown removes the last edge")}},{name:"EVALSHA memoization: stable script body → one sha, EVALSHA hits, NOSCRIPT falls back to EVAL (§7.8)",fn:async()=>{let $=0,Q=0,K=new Set,Z=v7({async eval(H,J,j){return $+=1,K.add(D9(H)),"ok"},async evalsha(H,J,j){if(Q+=1,!K.has(H))throw Error("NOSCRIPT No matching script. Please use EVAL.");return"ok"}}),G=`redis.call('SET', KEYS[1], ARGV[1])
1
+ import{spawn as K8}from"node:child_process";import{trimTrailingSlash as Y8}from"@ionite/core";function Z8(X){return new Promise(($)=>setTimeout($,X))}var X8=5000,z8=2000;function L$(X,$){if(X.exitCode!==null||X.signalCode!==null)return Promise.resolve(!0);return new Promise((Q)=>{let K=!1,Y=()=>{if(K)return;K=!0,clearTimeout(Z),Q(!0)},Z=setTimeout(()=>{if(K)return;K=!0,X.removeListener("exit",Y),Q(!1)},$);X.once("exit",Y)})}var G8="/api/ionite";function L1(X){if(X.readyPath)return X.readyPath;return`${Y8(X.basePath??G8)}/readyz`}async function E1(X,$={}){let{livenessPath:Q,maxRetries:K=30,retryDelayMs:Y=1000,requestTimeoutMs:Z=3000}=$,G=L1($),W;for(let J=0;J<K;J++){try{if(Q){let D=await fetch(`${X}${Q}`,{signal:AbortSignal.timeout(Z)});if(!D.ok)throw Error(`liveness returned ${D.status}`)}let j=await fetch(`${X}${G}`,{signal:AbortSignal.timeout(Z)});if(j.ok)return;W=Error(`readiness returned ${j.status}`)}catch(j){W=j}if(J<K-1)await Z8(Y)}let H=W instanceof Error?W.message:String(W);throw Error(`App not ready at ${X}${G} after ${K} attempts: ${H}`)}async function W8(X){let{command:$,args:Q=[],cwd:K,env:Y,baseUrl:Z,...G}=X,W=process.platform!=="win32",H=K8($,Q,{cwd:K,env:{...process.env,...Y},stdio:"inherit",detached:W});H.on("error",(P)=>{console.error(`Failed to start app under test (${$}):`,P)});let J=(P)=>{try{if(W&&H.pid!==void 0)process.kill(-H.pid,P);else H.kill(P)}catch{}},j=()=>J("SIGKILL");process.once("exit",j);let D=async()=>{if(H.exitCode===null&&H.signalCode===null){if(J("SIGTERM"),!await L$(H,X8))J("SIGKILL"),await L$(H,z8)}J("SIGKILL"),process.removeListener("exit",j)};try{await E1(Z,G)}catch(P){throw await D(),P}return{baseUrl:Z,...H.pid===void 0?{}:{pid:H.pid},stop:D}}import{accessRefId as V$,accessRootId as x0,assertRequiredSubjectType as H8,CatalogAdjacencyCache as N$,deepEqualPlain as G5,defineCatalog as W5,escapeScimFilterValue as h1,expandRoot as d0,hasAllScope as P2,hasAnyScope as T$,hasPermission as _0,hasScope as q2,InMemoryAccessAssignmentStore as v,InMemoryAccessCatalogStore as Z0,InMemoryAccessIndex as U,InMemoryResourceStore as F2,InMemoryUserStore as J8,ION_AUTHZ_GROUP_SCHEMA as W0,ION_AUTHZ_OAA_SYNC as j2,ION_IAM_GROUPS_WRITE as V0,ION_IAM_READ as H1,ION_IAM_USERS_WRITE as x$,ION_TENANT_MANAGE as k1,ionite as _,stableConfigHash as N1,unionCatalogRead as O$}from"@ionite/server";import{createValidators as R}from"@ionite/valibot";function T0(X){return typeof X.baseUrl==="function"?X.baseUrl():X.baseUrl}function V1(X,$){let Q=typeof X==="function"?X():X;if(!Q)throw Error(`${$} is required`);return Q}function r(X){let $=X.timeout??5000;return async function(K,Y={}){let Z=T0(X),G=K.startsWith("http://")||K.startsWith("https://")?K:new URL(K,Z).href,W=new AbortController,H=setTimeout(()=>W.abort(),$),J=typeof X.headers==="function"?await X.headers():X.headers??{};try{return await fetch(G,{...Y,signal:W.signal,headers:{...J,...Y.headers},redirect:"manual"})}finally{clearTimeout(H)}}}async function N(X,$){let Q=performance.now();try{let K=await $();return{name:X,passed:!0,duration:performance.now()-Q,details:K?.details}}catch(K){return{name:X,passed:!1,error:K instanceof Error?K.message:String(K),duration:performance.now()-Q}}}function W1(X,$){return{name:X,passed:!0,duration:0,details:{skipped:!0,reason:$}}}function z(X,$){if(!X)throw Error($)}function w(X,$,Q){if(X!==$)throw Error(Q??`Expected ${JSON.stringify($)}, got ${JSON.stringify(X)}`)}function r0(X,$,Q){let K=$["~standard"].validate(X);if(K instanceof Promise)throw Error(Q??"Async validators are not supported in assertValid. Use the validator directly with await.");if("issues"in K){let Z=K.issues.map((G)=>{let W=G.path?G.path.map(String).join("."):"";return W?`${W}: ${G.message}`:G.message});throw Error(Q??`Validation failed: ${Z.join("; ")}`)}}function y0(X){let $=new Map,Q=X.getSetCookie?.()??[];for(let K of Q){let[Y]=K.split(";"),[Z,G]=Y.split("=");if(Z&&G!==void 0)$.set(Z.trim(),G.trim())}return $}function f0(X){return Array.from(X.entries()).map(([$,Q])=>`${$}=${Q}`).join("; ")}function h(X){return{async load(){return X},async subscribe(){return},secret:{}}}async function E(){let X=new Z0;await X.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await X.permissions.set({id:"write",name:"Write",description:"Write",source:"app"}).ready(),await X.permissions.set({id:"manage",name:"Manage",description:"Manage (cascades to sub-resources)",applyToSubResources:!0,source:"app"}).ready(),await X.roles.set({id:"editor",name:"Editor",description:"Editor",permissions:["read","write"],source:"app"}).ready(),await X.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await X.roles.set({id:"ops",name:"Ops",description:"Ops",permissions:["manage"],source:"app"}).ready(),await X.groups.set({id:"staff",providerId:"iam-staff",name:"Staff",description:"Staff",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],source:"app"}).ready(),await X.groups.set({id:"internal_staff",providerId:"iam-internal-staff",name:"Internal Staff",description:"Internal composition group",roles:["editor"],source:"app"}).ready();let $=new F2;return $.set({id:"project-a",type:"project",name:"Project A",description:"A",resources:["module-1"]}),$.set({id:"module-1",type:"module",name:"Module 1",description:"M1",parent:"project-a"}),$.set({id:"region:north",type:"region",name:"North Region",description:"North"}),$.set({id:"region:south",type:"region",name:"South Region",description:"South"}),await X.groups.set({id:"region_north_librarian",name:"North Region Librarian",description:"Requestable entitlement scoped to the North region",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],resources:["region:north"],source:"app"}).ready(),{catalog:X,resources:$}}function J1(){return W5({permissions:[{id:"code_read",name:"Code Read",description:"Read (code-declared)"},{id:"code_write",name:"Code Write",description:"Write (code-declared)"}],roles:[{id:"code_editor",name:"Code Editor",description:"Editor (code-declared)",permissions:["code_read","code_write"]}],groups:[{id:"code_staff",name:"Code Staff",description:"Staff (code-declared)",requestable:!0,assignableSubjectTypes:["user"],roles:["code_editor"]}]})}async function A(X,$,Q,K){let Y={...K,id:x0(K),status:"active"};await X.grant(Y);let Z=await d0(Y,Q);return await $.materialize(Y.id,Z),Y.id}class H5 extends U{failNext=!0;materialize(X,$){if(this.failNext)return this.failNext=!1,Promise.reject(Error("simulated transient index failure"));return super.materialize(X,$)}}class _$ extends U{replacementStartedResolve;replacementReleaseResolve;replacementStarted=new Promise((X)=>{this.replacementStartedResolve=X});replacementRelease=new Promise((X)=>{this.replacementReleaseResolve=X});async replaceSubject(X,$,Q){this.replacementStartedResolve(),await this.replacementRelease,await super.replaceSubject(X,$,Q)}releaseReplacement(){this.replacementReleaseResolve()}}class R$ extends J8{upsert(X){return Promise.reject(Error("simulated user metadata write failure"))}}function J5(){return{tests:[{name:"grant a group root materializes the full closure (group -> role -> permissions)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"alice",subjectType:"user",targetType:"group",target:"staff"}),z((await K.hasGroup("alice","staff",void 0,"user")).allow,"alice should be in group staff"),z((await K.hasRole("alice","editor",void 0,"user")).allow,"staff confers role editor"),z((await K.hasPermission("alice","read",void 0,"user")).allow,"editor confers permission read"),z((await K.hasPermission("alice","write",void 0,"user")).allow,"editor confers permission write"),z(!(await K.hasPermission("alice","manage",void 0,"user")).allow,"alice should not have manage")}},{name:"diamond survival: revoking one role root keeps permissions reachable via another root",fn:async()=>{let $=await E(),Q=new v,K=new U,Y=await A(Q,K,$,{subject:"bob",subjectType:"user",targetType:"role",target:"editor"}),Z=await A(Q,K,$,{subject:"bob",subjectType:"user",targetType:"role",target:"viewer"});z((await K.hasPermission("bob","read",void 0,"user")).allow,"read present via both roles"),await Q.revoke({subject:"bob",subjectType:"user",targetType:"role",target:"editor"}),await K.removeByRoot(Y),z((await K.hasPermission("bob","read",void 0,"user")).allow,"read survives via viewer root"),z(!(await K.hasPermission("bob","write",void 0,"user")).allow,"write removed with editor root"),await Q.revoke({subject:"bob",subjectType:"user",targetType:"role",target:"viewer"}),await K.removeByRoot(Z),z(!(await K.hasPermission("bob","read",void 0,"user")).allow,"read gone once last root removed")}},{name:"applyToSubResources expands a resource-scoped permission across the sub-resource tree",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"carol",subjectType:"user",targetType:"permission",target:"manage",resources:["project-a"]}),z((await K.hasPermission("carol","manage","project-a","user")).allow,"manage on scoped resource"),z((await K.hasPermission("carol","manage","module-1","user")).allow,"manage cascades to sub-resource"),z(!(await K.hasPermission("carol","manage","other","user")).allow,"manage not on unrelated resource"),z((await K.hasPermission("carol","manage",void 0,"user")).allow,"manage present for some resource")}},{name:"clear(subject) removes only that subject derived edges",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"dave",subjectType:"user",targetType:"role",target:"viewer"}),await A(Q,K,$,{subject:"erin",subjectType:"user",targetType:"role",target:"viewer"}),await K.clear("dave","user"),z(!(await K.hasPermission("dave","read",void 0,"user")).allow,"dave edges cleared"),z((await K.hasPermission("erin","read",void 0,"user")).allow,"erin edges retained")}},{name:"AccessAssignmentStore.list returns granted active roots",fn:async()=>{let $=new v;await $.grant({subject:"frank",subjectType:"user",targetType:"role",target:"viewer",status:"active"}),await $.grant({subject:"frank",subjectType:"user",targetType:"role",target:"editor",status:"active"});let Q=await $.list();z(Q.items.length===2,"both granted roots should be listed"),z(Q.items.every((K)=>K.targetType==="role"),"listed roots are roles")}},{name:"listByTarget returns active members of an entitlement (SCIM member projection source)",fn:async()=>{let $=new v;await $.grant({subject:"alice",subjectType:"user",targetType:"group",target:"staff",status:"active"}),await $.grant({subject:"bob",subjectType:"user",targetType:"group",target:"staff",status:"active"}),await $.grant({subject:"carol",subjectType:"user",targetType:"role",target:"viewer",status:"active"});let Q=await $.listByTarget("group","staff"),K=Q.items.map((Z)=>Z.subject).sort();z(Q.items.length===2,"both staff members are projected"),z(K[0]==="alice"&&K[1]==="bob","staff members are alice and bob"),z(Q.items.every((Z)=>Z.target==="staff"),"every projected root targets staff");let Y=await $.listByTarget("role","viewer");z(Y.items.length===1&&Y.items[0]?.subject==="carol","viewer has only carol")}},{name:"group delete semantics: revoking each listByTarget root removes membership from the closure",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"grace",subjectType:"user",targetType:"group",target:"staff"}),await A(Q,K,$,{subject:"heidi",subjectType:"user",targetType:"group",target:"staff"}),z((await K.hasPermission("grace","read",void 0,"user")).allow,"grace inherits read via staff"),z((await K.hasPermission("heidi","read",void 0,"user")).allow,"heidi inherits read via staff");let Y=await Q.listByTarget("group","staff");for(let Z of Y.items)await Q.revoke(Z),await K.removeByRoot(Z.id??x0(Z));z(!(await K.hasGroup("grace","staff",void 0,"user")).allow,"grace no longer in staff"),z(!(await K.hasPermission("heidi","read",void 0,"user")).allow,"heidi loses inherited read"),z((await Q.listByTarget("group","staff")).items.length===0,"no staff membership roots remain")}},{name:"role/permission entitlement roots round-trip through listByTarget by type",fn:async()=>{let $=new v;await $.grant({subject:"ivan",subjectType:"user",targetType:"role",target:"admin",status:"active"}),await $.grant({subject:"judy",subjectType:"user",targetType:"permission",target:"admin",status:"active"});let Q=await $.listByTarget("role","admin"),K=await $.listByTarget("permission","admin");z(Q.items.length===1&&Q.items[0]?.subject==="ivan","role admin has ivan"),z(K.items.length===1&&K.items[0]?.subject==="judy","permission admin has judy")}},{name:"resource-bound group entitlement scopes the conferred role/permissions to its resources (member-add)",fn:async()=>{let $=await E(),Q=new v,K=new U,Z=((await $.catalog.groups.get("region_north_librarian"))?.resources??[]).map((G)=>typeof G==="string"?G:G.id);z(Z.length===1&&Z[0]==="region:north","entitlement carries the region binding"),await A(Q,K,$,{subject:"nina",subjectType:"user",targetType:"group",target:"region_north_librarian",resources:Z}),z((await K.hasRole("nina","editor","region:north","user")).allow,"editor scoped to region:north"),z((await K.hasPermission("nina","read","region:north","user")).allow,"read scoped to region:north"),z((await K.hasPermission("nina","write","region:north","user")).allow,"write scoped to region:north"),z(!(await K.hasPermission("nina","read","region:south","user")).allow,"access does not leak to a different region"),z(!(await K.hasRole("nina","editor","region:south","user")).allow,"role does not leak to region:south")}},{name:"AccessCatalogStore list honors the requestable filter (SCIM assignable set)",fn:async()=>{let{catalog:$}=await E(),Q=await $.groups.list({filter:{requestable:!0}});z(Q.items.every((Y)=>Y.requestable===!0),"requestable filter returns only requestable groups"),z(Q.items.some((Y)=>Y.id==="region_north_librarian"),"the requestable region entitlement is included"),z(!Q.items.some((Y)=>Y.id==="internal_staff"),"the internal staff group is excluded");let K=await $.groups.list({filter:{requestable:!1}});z(K.items.some((Y)=>Y.id==="internal_staff"),"internal_staff (requestable unset) is in the non-requestable set"),z(!K.items.some((Y)=>Y.id==="region_north_librarian"),"the requestable entitlement is excluded from the non-requestable set")}},{name:"AccessCatalogStore does not emit a reactive change for a value-identical set",fn:async()=>{let $=new Z0,Q=0,K=$.afterChange?.(()=>{Q+=1}),Y={id:"stable-group",name:"Stable Group",description:"No-op write proof",requestable:!0,metadata:{first:1,second:2}};await $.groups.set(Y).ready(),await $.groups.set({requestable:!0,description:"No-op write proof",name:"Stable Group",id:"stable-group",metadata:{second:2,first:1}}).ready(),K?.(),w(Q,1,"a key-reordered but value-identical set emits no second change")}},{name:"CodeCatalog union reads propagate backing-store failures instead of serving partial inventory",fn:async()=>{let $=async(W,H)=>{let J=!1;try{await W()}catch{J=!0}z(J,H)},Q=J1(),K=new Z0;K.groups.get=async()=>{throw Error("get unavailable")};let Y=O$(Q,K);z(Y!==void 0,"the code/store union is available"),await $(()=>Y.groups.get("code_staff"),"get failure must propagate");let Z=new Z0;Z.groups.list=async()=>{throw Error("list unavailable")};let G=O$(Q,Z);z(G!==void 0,"the code/store union is available"),await $(()=>G.groups.list(),"list failure must propagate")}},{name:"ion.authz.oaa becomes ready once OAA config (url/providerId/datasourceId) is applied",fn:async()=>{let $=_(h({id:"oaa-ready-test",authz:{oaa:{url:"https://veza.example",providerId:"prov",datasourceId:"ds"}}}),{validators:R()});z(!$.authz.oaa.isReady(),"OAA should not be ready before RemoteConfig is applied"),await $.ready(1000),z($.authz.oaa.isReady(),"OAA should be ready once url/providerId/datasourceId are present"),await $.authz.oaa.ready(1000),z($.readiness().modules.authz.ready===!0,"authz is ready once OAA is configured and applied"),$.close?.()}},{name:"ion.authz.oaa stays not ready and sync throws not_configured without OAA config",fn:async()=>{let $=_(h({id:"no-oaa-test"}),{validators:R()});await $.ready(1000),z(!$.authz.oaa.isReady(),"OAA should not be ready when no OAA config is present");let Q=$.readiness();z(Q.modules.authz.active===!0,"authz module is always active"),z(Q.modules.authz.ready===!0,"authz is ready when OAA is not configured"),z($.requireReady(["authz"]).ok===!0,"requireReady([authz]) is ok without OAA configured");let K;try{await $.authz.oaa.sync({})}catch(Y){K=Y.code}z(K==="not_configured","sync should throw OaaError with code not_configured when unconfigured"),$.close?.()}},{name:"ion.authz.has* delegate to the wired AccessIndex (allow / deny)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"alice",subjectType:"user",targetType:"role",target:"editor"});let Y=_(h({id:"authz-has-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasRole("alice","editor",void 0,"user")).allow,"ion.authz.hasRole resolves a granted role"),z((await Y.authz.hasPermission("alice","read",void 0,"user")).allow,"ion.authz.hasPermission resolves a permission conferred by the role"),z(!(await Y.authz.hasPermission("alice","manage",void 0,"user")).allow,"ion.authz.hasPermission denies a permission the subject was never granted"),Y.close?.()}},{name:"ion.authz.has* deny by default (not_configured) when no AccessIndex is wired",fn:async()=>{let $=_(h({id:"authz-noindex-test"}),{validators:R()});await $.ready(1000);let Q=await $.authz.hasPermission("alice","read",void 0,"user");z(!Q.allow,"denies when no AccessIndex is configured"),z(Q.metadata?.reason==="access_index_not_configured","decision metadata explains the missing AccessIndex"),$.close?.()}},{name:"standalone hasPermission(request, ion) returns unauthenticated -> 401 without a session",fn:async()=>{let $=_(h({id:"authz-helper-test"}),{validators:R()});await $.ready(1000);let Q=await _0(new Request("https://example.test/"),$,"read");z(!Q.allow,"no SSO session -> not allowed"),z(Q.reason==="unauthenticated","reason is unauthenticated when no user is resolved"),z(Q.response().status===401,"response() is 401 for the unauthenticated case"),$.close?.()}},{name:"subjectType isolates principals that share an id (user:123 vs customer:123)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"123",subjectType:"user",targetType:"role",target:"editor"}),await A(Q,K,$,{subject:"123",subjectType:"customer",targetType:"role",target:"viewer"}),z((await K.hasRole("123","editor",void 0,"user")).allow,"user:123 has editor"),z((await K.hasPermission("123","write",void 0,"user")).allow,"user:123 has write via editor"),z(!(await K.hasRole("123","editor",void 0,"customer")).allow,"customer:123 does NOT have editor"),z((await K.hasRole("123","viewer",void 0,"customer")).allow,"customer:123 has viewer"),z(!(await K.hasRole("123","viewer",void 0,"user")).allow,"user:123 does NOT have viewer"),z(!(await K.hasPermission("123","write",void 0,"customer")).allow,"customer:123 has no write"),await K.clear("123","user"),z(!(await K.hasRole("123","editor",void 0,"user")).allow,"user:123 edges cleared"),z((await K.hasRole("123","viewer",void 0,"customer")).allow,"customer:123 edges retained")}},{name:"ion.authz.has* honor subjectType (a customer grant is invisible to a user check)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"cust-1",subjectType:"customer",targetType:"role",target:"viewer"});let Y=_(h({id:"authz-subjecttype-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasRole("cust-1","viewer",void 0,"customer")).allow,"customer grant resolves under the customer subjectType"),z((await Y.authz.hasPermission("cust-1","read",void 0,"customer")).allow,"customer inherits read via viewer"),z(!(await Y.authz.hasRole("cust-1","viewer",void 0,"user")).allow,"same id under user subjectType is denied"),z(!(await Y.authz.hasRole("cust-1","viewer",void 0,"user")).allow,"same id under explicit user subjectType is denied"),Y.close?.()}},{name:"standalone has* default 'any' resolves no principals without a session -> unauthenticated/401",fn:async()=>{let $=await E(),Q=new v,K=new U,Y=_(h({id:"authz-any-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000);let Z=await _0(new Request("https://example.test/"),Y,"read");z(!Z.allow,"default 'any' with no session is not allowed"),z(Z.reason==="unauthenticated","no principal resolved -> unauthenticated"),z(Z.subject===void 0,"no subject is reported when nothing resolved"),z(Z.subjectType===void 0,"no subjectType is reported when nothing resolved"),z(Z.response().status===401,"unauthenticated -> 401"),Y.close?.()}},{name:"subjectType isolates a workload from a user that shares an id (workload:abc vs user:abc)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"abc",subjectType:"workload",targetType:"role",target:"ops"}),await A(Q,K,$,{subject:"abc",subjectType:"user",targetType:"role",target:"viewer"}),z((await K.hasRole("abc","ops",void 0,"workload")).allow,"workload:abc has ops"),z((await K.hasPermission("abc","manage",void 0,"workload")).allow,"workload:abc has manage via ops"),z(!(await K.hasRole("abc","ops",void 0,"user")).allow,"user:abc does NOT have ops"),z(!(await K.hasPermission("abc","manage",void 0,"user")).allow,"user:abc does NOT inherit manage"),z((await K.hasRole("abc","viewer",void 0,"user")).allow,"user:abc has viewer"),z(!(await K.hasRole("abc","viewer",void 0,"workload")).allow,"workload:abc does NOT have viewer"),await K.clear("abc","workload"),z(!(await K.hasRole("abc","ops",void 0,"workload")).allow,"workload:abc edges cleared"),z((await K.hasRole("abc","viewer",void 0,"user")).allow,"user:abc edges retained")}},{name:"ion.authz.has* honor the workload subjectType (a workload grant is invisible to a user check)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"svc-1",subjectType:"workload",targetType:"role",target:"ops"});let Y=_(h({id:"authz-workload-test"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasRole("svc-1","ops",void 0,"workload")).allow,"workload grant resolves under the workload subjectType"),z((await Y.authz.hasPermission("svc-1","manage",void 0,"workload")).allow,"workload inherits manage via ops"),z(!(await Y.authz.hasRole("svc-1","ops",void 0,"user")).allow,"same id under user subjectType is denied"),Y.close?.()}},{name:"accessRootId and store writes fail closed when subjectType is omitted",fn:async()=>{let $=!1;try{x0({subject:"x",targetType:"role",target:"viewer"})}catch(K){$=K instanceof Error&&K.message.includes("subjectType is required")}z($,"accessRootId rejects a missing subjectType instead of defaulting to user"),$=!1;try{H8(void 0)}catch(K){$=K instanceof Error&&K.message.includes("subjectType is required")}z($,"assertRequiredSubjectType rejects undefined");let Q=new v;$=!1;try{await Q.grant({subject:"x",targetType:"role",target:"viewer"})}catch(K){$=K instanceof Error&&K.message.includes("subjectType is required")}z($,"assignment store grant rejects a missing subjectType")}},{name:"standalone hasScope* return unauthenticated -> 401 when no workload token is present",fn:async()=>{let $=_(h({id:"authz-scope-noauth-test"}),{validators:R()});await $.ready(1000);let Q=new Request("https://example.test/"),K=await q2(Q,$,"oaa:sync","workload");z(!K.allow&&K.reason==="unauthenticated","hasScope with no token -> unauthenticated"),z(K.response().status===401,"hasScope unauthenticated -> 401");let Y=await T$(Q,$,["a","b"],"workload");z(!Y.allow&&Y.reason==="unauthenticated","hasAnyScope with no token -> unauthenticated");let Z=await P2(Q,$,["a","b"],"workload");z(!Z.allow&&Z.reason==="unauthenticated","hasAllScope with no token -> unauthenticated"),$.close?.()}},{name:"RemoteConfig.authz.scopesAsEntitlements is read into ion.config (gates has* scope auto-honor)",fn:async()=>{let $=_(h({id:"scopes-flag-on",authz:{scopesAsEntitlements:!0}}),{validators:R()});await $.ready(1000),z($.config?.authz?.scopesAsEntitlements===!0,"flag true is applied to ion.config"),$.close?.();let Q=_(h({id:"scopes-flag-off"}),{validators:R()});await Q.ready(1000),z(!Q.config?.authz?.scopesAsEntitlements,"flag defaults to falsy when unset (secure default)"),Q.close?.()}},{name:"ion.authz.reconcile() re-derives the closure after a role->permission catalog edit (§1.2a)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"drift",subjectType:"user",targetType:"role",target:"viewer"});let Y=_(h({id:"reconcile-roleedit"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasPermission("drift","read",void 0,"user")).allow,"viewer confers read"),z(!(await Y.authz.hasPermission("drift","write",void 0,"user")).allow,"viewer does not confer write yet"),await $.catalog.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read","write"]}).ready();let Z=await Y.authz.reconcile();z(Z.roots===1,"one live root re-expanded (only subject in the system)"),z((await Y.authz.hasPermission("drift","write",void 0,"user")).allow,"reconcile materializes the new write edge"),z((await Y.authz.hasPermission("drift","read",void 0,"user")).allow,"read still present after reconcile"),Y.close?.()}},{name:"ion.authz.reconcile() extends an applyToSubResources closure to a newly added child resource (§1.2a)",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"tree",subjectType:"user",targetType:"permission",target:"manage",resources:["project-a"]});let Y=_(h({id:"reconcile-tree"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),z((await Y.authz.hasPermission("tree","manage","module-1","user")).allow,"manage cascades to the existing child module-1"),z(!(await Y.authz.hasPermission("tree","manage","module-2","user")).allow,"module-2 does not exist yet"),$.resources.set({id:"module-2",type:"module",name:"Module 2",description:"M2",parent:"project-a"}),$.resources.set({id:"project-a",type:"project",name:"Project A",description:"A",resources:["module-1","module-2"]}),await Y.authz.reconcile(),z((await Y.authz.hasPermission("tree","manage","module-2","user")).allow,"reconcile cascades manage to the newly added child"),Y.close?.()}},{name:"ion.authz.reconcile() rebuilds set-attributed edges so an overlapping permission survives one revoke",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"olap",subjectType:"user",targetType:"role",target:"editor"}),await A(Q,K,$,{subject:"olap",subjectType:"user",targetType:"role",target:"viewer"});let Y=_(h({id:"reconcile-overlap"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),await Q.revoke({subject:"olap",subjectType:"user",targetType:"role",target:"editor"});let Z=await Y.authz.reconcile();z(Z.roots===1,"only the surviving viewer root remains live"),z((await Y.authz.hasPermission("olap","read",void 0,"user")).allow,"read survives via the viewer root"),z(!(await Y.authz.hasPermission("olap","write",void 0,"user")).allow,"write is removed with the editor root"),Y.close?.()}},{name:"reconcile atomically swaps a subject closure without a transient deny window",fn:async()=>{let $=await E(),Q=new v,K=new _$;await A(Q,K,$,{subject:"atomic",subjectType:"user",targetType:"role",target:"editor"});let Y=_(h({id:"reconcile-atomic"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Y.ready(1000),await Q.revoke({subject:"atomic",subjectType:"user",targetType:"role",target:"editor"});let Z={subject:"atomic",subjectType:"user",targetType:"role",target:"viewer",status:"active"};Z.id=x0(Z),await Q.grant(Z);let G=Y.authz.reconcile();await K.replacementStarted;try{z((await K.hasPermission("atomic","write",void 0,"user")).allow,"the previous complete closure remains visible while its replacement is pending")}finally{K.releaseReplacement()}await G,z((await K.hasPermission("atomic","read",void 0,"user")).allow,"the replacement viewer closure is committed"),z(!(await K.hasPermission("atomic","write",void 0,"user")).allow,"the stale editor-only permission disappears at the atomic swap"),Y.close?.()}},{name:"partial-failure retry converges: a failed index materialize is repaired by re-grant + re-materialize (replaces §1.2b)",fn:async()=>{let $=await E(),Q=new v,K=new H5,Y={subject:"retry",subjectType:"user",targetType:"role",target:"viewer",status:"active"};Y.id=x0(Y),await Q.grant(Y);let Z=!1;try{let W=await d0(Y,$);await K.materialize(Y.id,W)}catch{Z=!0}z(Z,"the first index write fails (simulated transient error)"),z(!(await K.hasPermission("retry","read",void 0,"user")).allow,"no derived access after the failed attempt"),await Q.grant(Y);let G=await d0(Y,$);await K.materialize(Y.id,G),z((await K.hasPermission("retry","read",void 0,"user")).allow,"retry re-materializes the closure"),z((await Q.listBySubject("retry","user")).items.length===1,"exactly one root persisted (grant is idempotent; no duplicate)"),await K.removeByRoot(Y.id),z(!(await K.hasPermission("retry","read",void 0,"user")).allow,"access cleanly removed; no orphan rows linger")}},{name:"revoke idempotency: revoking an absent root is a no-op and a retried revoke still denies",fn:async()=>{let $=await E(),Q=new v,K=new U,Y=await A(Q,K,$,{subject:"idem",subjectType:"user",targetType:"role",target:"viewer"});z((await K.hasPermission("idem","read",void 0,"user")).allow,"granted read is present"),await Q.revoke({subject:"idem",subjectType:"user",targetType:"role",target:"viewer"}),await K.removeByRoot(Y),z(!(await K.hasPermission("idem","read",void 0,"user")).allow,"read denied after revoke"),await Q.revoke({subject:"idem",subjectType:"user",targetType:"role",target:"viewer"}),await K.removeByRoot(Y),z((await Q.listBySubject("idem","user")).items.length===0,"no roots remain after idempotent revoke"),z(!(await K.hasPermission("idem","read",void 0,"user")).allow,"retry of an absent revoke keeps access denied")}},{name:"user delete semantics (§1.19): revoking each listBySubject root removes the subject from the closure",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"leaver",subjectType:"user",targetType:"group",target:"staff"}),await A(Q,K,$,{subject:"leaver",subjectType:"user",targetType:"role",target:"ops"}),z((await K.hasPermission("leaver","read",void 0,"user")).allow,"leaver inherits read via staff"),z((await K.hasPermission("leaver","manage",void 0,"user")).allow,"leaver holds manage via ops");let Y=await Q.listBySubject("leaver","user");z(Y.items.length===2,"both of the leaver roots are enumerated by listBySubject");for(let Z of Y.items)await Q.revoke(Z),await K.removeByRoot(Z.id??x0(Z));z(!(await K.hasGroup("leaver","staff",void 0,"user")).allow,"group membership revoked on delete"),z(!(await K.hasPermission("leaver","read",void 0,"user")).allow,"inherited read denied on delete"),z(!(await K.hasPermission("leaver","manage",void 0,"user")).allow,"role permission denied on delete"),z((await Q.listBySubject("leaver","user")).items.length===0,"no assignment roots remain")}},{name:"ion.authz.reconcile() rebuilds live subjects from roots and sweeps orphan index rows",fn:async()=>{let $=await E(),Q=new v,K=new U,Y={subject:"live",subjectType:"user",targetType:"role",target:"editor",status:"active"};Y.id=x0(Y),await Q.grant(Y),z(!(await K.hasPermission("live","read",void 0,"user")).allow,"live subject has no index rows pre-reconcile"),await K.materialize("orphan-root",[{subject:"orphan",subjectType:"user",targetType:"role",target:"viewer",rootId:"orphan-root"},{subject:"orphan",subjectType:"user",targetType:"permission",target:"read",rootId:"orphan-root"}]),z((await K.hasPermission("orphan","read",void 0,"user")).allow,"orphan rows are present pre-reconcile");let Z=_(h({id:"reconcile-full"}),{validators:R(),stores:{accessStore:Q,accessIndex:K,catalogStore:$.catalog,resources:$.resources}});await Z.ready(1000);let G=await Z.authz.reconcile();z(G.subjects>=2,"the full reconcile visited both the live and orphan subjects"),z((await Z.authz.hasPermission("live","read",void 0,"user")).allow,"full reconcile materializes the live root"),z((await Z.authz.hasPermission("live","write",void 0,"user")).allow,"editor closure rebuilt for the live subject"),z(!(await Z.authz.hasPermission("orphan","read",void 0,"user")).allow,"orphan index rows are swept"),z(G.cleared>=1,"the orphan subject is reported as cleared"),Z.close?.()}},{name:"§8.4 id parity: two roots with the same subject/target but different resources do NOT collapse",fn:async()=>{let $={subject:"pat",subjectType:"user",targetType:"role",target:"editor",resources:["region:north"]},Q={subject:"pat",subjectType:"user",targetType:"role",target:"editor",resources:["region:south"]};z(x0($)!==x0(Q),"different resource scopes yield different root ids"),z(x0($)===x0({...$}),"same inputs yield a stable root id");let K=new v;await K.grant({...$,id:x0($),status:"active"}),await K.grant({...Q,id:x0(Q),status:"active"});let Y=await K.listByTarget("role","editor");z(Y.items.length===2,"both resource-scoped roots coexist (no collapse)"),z(V$(" editor ")==="editor","accessRefId trims string ids"),z(V$({id:"editor"})==="editor","accessRefId resolves an object ref by id")}},{name:"§7.4 listByTargets batches a page of member projections into one store call (no N+1)",fn:async()=>{let $=0,Q=0;class K extends v{async listByTarget(...G){return $+=1,super.listByTarget(...G)}async listByTargets(...G){return Q+=1,super.listByTargets(...G)}}let Y=new K;await Y.grant({subject:"m1",subjectType:"user",targetType:"group",target:"g1",status:"active"}),await Y.grant({subject:"m2",subjectType:"user",targetType:"group",target:"g1",status:"active"}),await Y.grant({subject:"m3",subjectType:"user",targetType:"role",target:"r1",status:"active"});let Z=await Y.listByTargets([{targetType:"group",target:"g1"},{targetType:"role",target:"r1"},{targetType:"permission",target:"p1"}]);z(Q===1,"one batched call covers the whole page"),z($===0,"the batched path does not fan out to per-item listByTarget"),z((Z.get("group:g1")??[]).length===2,"g1 members batched"),z((Z.get("role:r1")??[]).length===1,"r1 members batched"),z((Z.get("permission:p1")??[]).length===0,"an empty target returns an empty list")}},{name:"§7.4 hasAll over N targets resolves in one pass (bounded) and denies if any is missing",fn:async()=>{let $=await E(),Q=new v,K=new U;await A(Q,K,$,{subject:"multi",subjectType:"user",targetType:"role",target:"editor"}),await A(Q,K,$,{subject:"multi",subjectType:"user",targetType:"role",target:"viewer"}),await A(Q,K,$,{subject:"multi",subjectType:"user",targetType:"role",target:"ops"});let Y=["editor","viewer","ops"].map((G)=>({targetType:"role",target:G}));z((await K.hasAll("multi",Y,"user")).allow,"hasAll is satisfied when every target is held");let Z=[...Y,{targetType:"role",target:"admin"}];z(!(await K.hasAll("multi",Z,"user")).allow,"hasAll denies when one target is missing")}},{name:"cross-tenant isolation: grants in one store keyspace are invisible to another (distinct stores)",fn:async()=>{let $=await E(),Q={store:new v,index:new U},K={store:new v,index:new U};await A(Q.store,Q.index,$,{subject:"shared-123",subjectType:"user",targetType:"role",target:"ops"}),z((await Q.index.hasRole("shared-123","ops",void 0,"user")).allow,"tenant A granted ops to shared-123"),z(!(await K.index.hasRole("shared-123","ops",void 0,"user")).allow,"tenant B (separate keyspace) does NOT see tenant A grant for the same id"),z((await K.store.listBySubject("shared-123","user")).items.length===0,"tenant B store has no roots for the shared id")}},{name:"§7.4 CatalogAdjacencyCache memoizes catalog reads and re-reads after invalidation (same edges)",fn:async()=>{let $=await E(),Q={subject:"memo",subjectType:"user",targetType:"group",target:"staff"};Q.id=x0(Q);let K=await d0(Q,$),Y=new N$($.catalog,$.resources),Z={...$,cache:Y},G=await d0(Q,Z),W=await d0(Q,Z);z(G5(K,G)&&G5(G,W),"cached expansion produces the same edges as the uncached read"),await $.catalog.groups.set({id:"staff",name:"Staff",description:"Staff",roles:["editor","ops"]}).ready();let H=await d0(Q,Z);z(G5(H,G),"a not-yet-invalidated memo still returns the old closure"),Y.invalidate();let J=await d0(Q,Z);z(J.some((j)=>j.targetType==="permission"&&j.target==="manage"),"after invalidation the cache re-reads the catalog and reflects the new ops->manage edge")}},{name:"code-declared catalog (defineCatalog) expands a grant with NO catalogStore wired (zero store reads)",fn:async()=>{let $=J1(),Q=new v,K=new U;await A(Q,K,{code:$},{subject:"carol",subjectType:"user",targetType:"group",target:"code_staff"}),z((await K.hasGroup("carol","code_staff",void 0,"user")).allow,"carol is in the code-declared group"),z((await K.hasRole("carol","code_editor",void 0,"user")).allow,"the code group confers the code role"),z((await K.hasPermission("carol","code_read",void 0,"user")).allow,"the code role confers code_read"),z((await K.hasPermission("carol","code_write",void 0,"user")).allow,"the code role confers code_write"),z($.groups.get("code_staff")?.id==="code_staff","the App-owned catalog id remains stable")}},{name:"defineCatalog rejects duplicate ids, dangling references, and group cycles at module evaluation",fn:async()=>{let $=(Q)=>{try{return Q(),!1}catch{return!0}};z($(()=>W5({roles:[{id:"dup",name:"Dup",description:"dup"}],permissions:[{id:"dup",name:"Dup",description:"dup"}]})),"a duplicate id across kinds throws (one SCIM Groups id space)"),z($(()=>W5({groups:[{id:"g",name:"G",description:"g",roles:["missing_role"]}]})),"a dangling group->role reference throws"),z($(()=>W5({groups:[{id:"a",name:"A",description:"a",groups:["b"]},{id:"b",name:"B",description:"b",groups:["a"]}]})),"a nested-group cycle throws")}},{name:"§7.2 stableConfigHash is order-independent and agrees with deepEqualPlain equality",fn:async()=>{let $={id:"t",authz:{scopesAsEntitlements:!0},workload:{jwksUri:"https://x/certs"}},Q={workload:{jwksUri:"https://x/certs"},authz:{scopesAsEntitlements:!0},id:"t"},K={...$,extra:void 0},Y={...$,id:"t2"};z(G5($,Q),"precondition: the two objects are deep-equal"),z(N1($)===N1(Q),"key order does not change the hash"),z(N1($)===N1(K),"undefined-valued keys do not change the hash"),z(N1($)!==N1(Y),"a changed value changes the hash")}},{name:'§10.3 escapeScimFilterValue backslash-escapes `"` and `\\` for RFC 7644 filters',fn:async()=>{w(h1('a"b'),"a\\\"b","a double quote is backslash-escaped so it stays inside the filter literal"),w(h1("a\\b"),"a\\\\b","a backslash is doubled so the escape is unambiguous"),w(h1("x\\\"y"),"x\\\\\\\"y","ordering escapes backslash first, then quote"),w(h1("plain-id"),"plain-id","an ordinary id is returned unchanged");let $=`externalId eq "${h1('ev"il')}"`;w($,'externalId eq "ev\\"il"',"the composed filter carries the escaped literal")}},{name:"§7.3 a shared CatalogAdjacencyCache collapses N member expansions to O(graph) catalog reads",fn:async()=>{let $=new Z0;await $.permissions.set({id:"read",name:"Read",description:"Read"}).ready(),await $.permissions.set({id:"write",name:"Write",description:"Write"}).ready(),await $.roles.set({id:"editor",name:"Editor",description:"Editor",permissions:["read","write"]}).ready(),await $.groups.set({id:"staff",name:"Staff",description:"Staff",roles:["editor"]}).ready();let Q=0,K=(j)=>({...j,get:async(D)=>{return Q+=1,j.get(D)}}),Y={groups:K($.groups),roles:K($.roles),permissions:K($.permissions)},Z=Array.from({length:100},(j,D)=>`m${D}`),G=(j)=>({subject:j,subjectType:"user",targetType:"group",target:"staff",status:"active",id:`r-${j}`});Q=0;for(let j of Z)await d0(G(j),{catalog:Y});let W=Q,H=new N$(Y);Q=0;for(let j of Z)await d0(G(j),{catalog:Y,cache:H});let J=Q;z(W>=Z.length,`uncached expansion re-reads the graph per member (saw ${W})`),z(J<W,"a shared adjacency cache must collapse repeated node reads"),z(J<=8,`cached expansion reads the shared graph O(graph) once, not O(members×graph) (saw ${J})`)}}]}}function v$(X){let $=typeof X==="function"?X():X;if(!$)throw Error("AuthzValidationConfig.ecv is required to run OAA projection tests");return $}function L2(X,$){return h({id:"ecv-oaa-projection",authz:{oaa:{url:X.baseUrl,apiKey:X.oaa.apiKey,providerId:X.oaa.providerId,datasourceId:X.oaa.datasourceId,applicationName:"ECV OAA Projection"},...$?{trustedProvisioningClients:$}:{}}})}async function l0(X,$,Q,K){let Y=_(L2(X,K),{validators:R()});try{return await Y.ready($),await Q(Y)}finally{Y.close?.()}}async function w2(X,$,Q,K,Y){let Z=_(L2(X),{validators:R(),stores:{catalogStore:Q,...Y?{resources:Y}:{}}});try{return await Z.ready($),await K(Z)}finally{Z.close?.()}}function C1(X,$){return h({id:"ecv-oaa-fullsync",workload:{jwksUri:`${X.baseUrl}/workload/certs`,audience:`${X.baseUrl}/workload`},authz:{scopesAsEntitlements:!0,oaa:{url:X.baseUrl,apiKey:X.oaa.apiKey,providerId:X.oaa.providerId,datasourceId:X.oaa.datasourceId,applicationName:"ECV OAA Full Sync",fullSync:{enabled:$}}}})}function D2(X,$){return h({id:$,workload:{jwksUri:`${X.baseUrl}/workload/certs`,audience:`${X.baseUrl}/workload`},iam:{},authz:{scopesAsEntitlements:!0,oaa:{url:X.baseUrl,apiKey:X.oaa.apiKey,providerId:X.oaa.providerId,datasourceId:X.oaa.datasourceId,applicationName:"ECV SCIM OAA Guard"}}})}var O1="https://ecv.test/api/ionite/authz/oaa/sync";function Y0(X){let $=X.applications;if(!Array.isArray($)||$.length===0)return;let Q=$[0];return Q&&typeof Q==="object"?Q:void 0}function y(X){if(!Array.isArray(X))return[];return X.filter(($)=>Boolean($)&&typeof $==="object")}function j5(X){let $=X.timeout??5000,Q=()=>v$(X.ecv);return[{name:"SCIM Group member type Workload grants a workload root and round-trips as type Workload",fn:async()=>{let K=await S(Q().baseUrl,`${V0.id} ${H1.id}`),Y=new Z0;await Y.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await Y.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await Y.groups.set({id:"vault_path_reader",name:"Path Reader",description:"Path Reader",requestable:!0,assignableSubjectTypes:["workload"],roles:["viewer"],source:"app"}).ready();let Z=new v,G=new U,W=_(h({id:"ecv-scim-workload-member",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y,accessStore:Z,accessIndex:G}});try{await W.ready(1000);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/vault_path_reader",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-vault",type:"Workload"}]}]})}));z(H.status===200,`workload member PATCH should succeed, got ${H.status}: ${await H.text()}`),z((await G.hasPermission("svc-vault","read",void 0,"workload")).allow,"Workload member satisfies a workload permission check"),z(!(await G.hasPermission("svc-vault","read",void 0,"user")).allow,"same-id user check does not see the workload grant");let J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/vault_path_reader",{headers:{Authorization:`Bearer ${K}`}}));z(J.status===200,`GET group should succeed, got ${J.status}`);let D=(await J.json()).members?.find((P)=>P.value==="svc-vault");z(D?.type==="Workload",`projected member type should be Workload, got ${D?.type}`)}finally{W.close?.()}}},{name:"SCIM typeless member resolves when assignableSubjectTypes has exactly one kind and is rejected when ambiguous",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0;await Y.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await Y.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await Y.groups.set({id:"workload_only",name:"Workload Only",description:"Workload Only",requestable:!0,assignableSubjectTypes:["workload"],roles:["viewer"],source:"app"}).ready(),await Y.groups.set({id:"open_entitlement",name:"Open Entitlement",description:"Open Entitlement",requestable:!0,roles:["viewer"],source:"app"}).ready();let Z=new v,G=new U,W=_(h({id:"ecv-scim-typeless-member",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y,accessStore:Z,accessIndex:G}});try{await W.ready(1000);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/workload_only",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-typeless"}]}]})}));z(H.status===200,`typeless member on a workload-only entitlement should resolve, got ${H.status}: ${await H.text()}`),z((await G.hasPermission("svc-typeless","read",void 0,"workload")).allow,"typeless member resolved to workload from assignableSubjectTypes");let J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/open_entitlement",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-ambiguous"}]}]})}));z(J.status===400,`typeless member on an unrestricted entitlement should 400, got ${J.status}`);let j=await J.text();z(j.includes("open_entitlement"),"ambiguity error names the entitlement"),z((await Z.listBySubject("svc-ambiguous","user")).items.length===0,"ambiguous member wrote no user root"),z((await Z.listBySubject("svc-ambiguous","workload")).items.length===0,"ambiguous member wrote no workload root")}finally{W.close?.()}}},{name:"same-id User and Workload members remain distinct roots",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0;await Y.permissions.set({id:"read",name:"Read",description:"Read",source:"app"}).ready(),await Y.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app"}).ready(),await Y.groups.set({id:"shared_id_entitlement",name:"Shared Id Entitlement",description:"Shared Id Entitlement",requestable:!0,assignableSubjectTypes:["user","workload"],roles:["viewer"],source:"app"}).ready();let Z=new v,G=new U,W=_(h({id:"ecv-scim-same-id-members",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y,accessStore:Z,accessIndex:G}});try{await W.ready(1000);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/shared_id_entitlement",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"shared",type:"User"},{value:"shared",type:"Workload"}]}]})}));z(H.status===200,`same-id User+Workload PATCH should succeed, got ${H.status}: ${await H.text()}`),z((await G.hasPermission("shared","read",void 0,"user")).allow,"user:shared holds the grant"),z((await G.hasPermission("shared","read",void 0,"workload")).allow,"workload:shared holds a distinct grant");let J=await Z.listBySubject("shared","user"),j=await Z.listBySubject("shared","workload");z(J.items.length===1,"user principal has its own root"),z(j.items.length===1,"workload principal has its own root"),z(J.items[0]?.id!==j.items[0]?.id,"same-id User and Workload roots are distinct")}finally{W.close?.()}}},{name:"ion.authz.oaa is ready when pointed at the mock receiver and projects an incremental user push",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(W)=>{z(W.authz.oaa.isReady(),"OAA should be ready with mock receiver config"),await W.authz.oaa.ready($),await W.authz.oaa.pushUser({id:"ecv-user-1",userName:"ecv-user-1@example.com",displayName:"ECV User One",email:"ecv-user-1@example.com"})});let K=await Q().oaa.pushes();z(K.length>=1,"expected at least one OAA push to be received");let Y=K[K.length-1];z(Y!==void 0,"expected a recorded OAA push"),z(Y.via==="single","an incremental user push should use the single :push endpoint"),z(Y.isIncremental===!0,"a mutation-scoped push should be marked incremental"),z(Y.compressionType==="GZIP","OAA client compresses payloads with gzip by default");let Z=Y0(Y.payload);z(Z!==void 0,"OAA payload must contain an application");let G=y(Z?.local_users);z(G.some((W)=>W.unique_id==="ecv-user-1"),"the pushed user should appear in local_users")}},{name:"ion.authz.oaa projects a group definition with its role/permission assignments",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(H)=>{await H.authz.oaa.pushGroup({id:"ecv-group-1",name:"ECV Group One",description:"ECV projected group",roles:["ecv-role-1"],permissions:["ecv-perm-1"]})});let K=(await Q().oaa.pushes()).at(-1);z(K!==void 0,"expected a recorded OAA group push");let Y=Y0(K.payload),Z=y(Y?.local_groups);z(Z.some((H)=>H.unique_id==="ecv-group-1"),"the pushed group should appear in local_groups");let W=y(K.payload.identity_to_permissions).find((H)=>H.identity==="ecv-group-1");z(W!==void 0,"group role/permission assignments should project an identity entry"),z(W?.identity_type==="local_group","a catalog group is projected as a local_group principal")}},{name:"ion.authz.oaa scopes a resource-bound group role assignment (apply_to_application:false + resources)",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(D)=>{await D.authz.oaa.pushGroup({id:"ecv-region-librarian",name:"ECV Region Librarian",description:"resource-bound requestable entitlement",requestable:!0,roles:["ecv-librarian"],resources:["region:ecv-north"]})});let K=(await Q().oaa.pushes()).at(-1);z(K!==void 0,"expected a recorded OAA group push");let Z=y(K.payload.identity_to_permissions).find((D)=>D.identity==="ecv-region-librarian");z(Z!==void 0,"resource-bound group should project an identity entry");let W=y(Z?.role_assignments).find((D)=>D.role==="ecv-librarian");z(W!==void 0,"the group should confer the librarian role"),z(W?.apply_to_application===!1,"a resource-bound assignment must not be application-wide"),z(Array.isArray(W?.resources)&&(W?.resources).includes("region:ecv-north"),"the role assignment carries the bound resource");let H=Y0(K.payload),j=y(H?.local_groups).find((D)=>D.unique_id==="ecv-region-librarian")?.custom_properties??{};z(Array.isArray(j.resources)&&j.resources.includes("region:ecv-north"),"group custom_properties expose the resource scope")}},{name:"ion.authz.oaa projects a permission with canonical permission types",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(G)=>{await G.authz.oaa.pushPermission({id:"ecv-perm-1",name:"ECV Read",description:"ECV read permission",permissionTypes:["DataRead"]})});let K=(await Q().oaa.pushes()).at(-1);z(K!==void 0,"expected a recorded OAA permission push");let Z=y(K.payload.permissions).find((G)=>G.name==="ECV Read");z(Z!==void 0,"the pushed permission should appear in the permissions stream"),z(Array.isArray(Z?.permission_type)&&(Z?.permission_type).includes("DataRead"),"permission_type should carry the canonical OAA privilege")}},{name:"ion.authz.oaa projects grant then revoke of a group membership",fn:async()=>{await Q().oaa.clear();let K={subject:"ecv-user-2",subjectType:"user",targetType:"group",target:"ecv-group-1"};await l0(Q(),$,async(J)=>{await J.authz.oaa.grant(K),await J.authz.oaa.revoke(K)});let Y=await Q().oaa.pushes();z(Y.length>=2,"grant and revoke should each produce a push");let G=y(Y0(Y.at(-2)?.payload??{})?.local_users).find((J)=>J.unique_id==="ecv-user-2");z(G!==void 0,"grant should project the subject as a local_user"),z(Array.isArray(G?.groups)&&(G?.groups).includes("ecv-group-1"),"granted membership should list the target group"),z(G?.operation==="add","grant projection uses the add operation");let H=y(Y0(Y.at(-1)?.payload??{})?.local_users).find((J)=>J.unique_id==="ecv-user-2");z(H?.operation==="delete","revoke projection uses the delete operation")}},{name:"ion.authz.oaa.sync uploads a full (non-incremental) snapshot",fn:async()=>{await Q().oaa.clear();let K=await l0(Q(),$,async(G)=>G.authz.oaa.sync({users:[{id:"ecv-sync-user",userName:"ecv-sync-user@example.com",displayName:"ECV Sync User",email:"ecv-sync-user@example.com"}],groups:[{id:"ecv-sync-group",name:"ECV Sync Group",description:"ECV sync group"}]}));z(K.success===!0,"full sync should report success from the receiver");let Y=(await Q().oaa.pushes()).at(-1);z(Y!==void 0,"expected a recorded OAA sync push"),z(Y.isIncremental===!1,"a full snapshot sync must not be marked incremental");let Z=Y0(Y.payload);z(Z!==void 0,"sync payload must contain an application"),z(y(Z?.local_users).some((G)=>G.unique_id==="ecv-sync-user"),"sync should include the snapshot users"),z(y(Z?.local_groups).some((G)=>G.unique_id==="ecv-sync-group"),"sync should include the snapshot groups")}},{name:"ion.authz.oaa.sync surfaces config trusted provisioning clients with eci-trusted provenance",fn:async()=>{await Q().oaa.clear();let K={issuer:"https://idp.example/realms/ops",audience:["ecv-app"],clientId:"ecv-provisioner",permissions:[k1.id]};await l0(Q(),$,async(j)=>j.authz.oaa.sync({}),[K]);let Y=(await Q().oaa.pushes()).at(-1);z(Y!==void 0,"expected a recorded OAA sync push"),z(Y.isIncremental===!1,"trusted-client union rides the full snapshot, not an incremental push");let Z=Y0(Y.payload),G=y(Z?.local_users).find((j)=>j.unique_id==="ecv-provisioner");z(G!==void 0,"the trusted provisioning client appears as a local_user");let W=G?.custom_properties??{};z(W.source==="eci-trusted","the trusted client carries eci-trusted provenance");let J=y(Y.payload.identity_to_permissions).find((j)=>j.identity==="ecv-provisioner");z(J!==void 0,"the trusted client projects its declared permission assignment")}},{name:"stored workload grants carry the OAA workload stamp and SCIM Workload member type",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,H1.id),Y=await E(),Z=new v,G=new U;await A(Z,G,Y,{subject:"ecv-reporting-workload",subjectType:"workload",targetType:"group",target:"region_north_librarian"});let W=(await Z.listByTarget("group","region_north_librarian")).items,H=_(D2(Q(),"ecv-stored-workload-projection"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:Z,accessIndex:G,resources:Y.resources}});try{await H.ready($);let J=W[0];z(J!==void 0,"expected the stored workload grant root"),await H.authz.oaa.grant(J);let j=(await Q().oaa.pushes()).at(-1);z(j!==void 0,"expected a recorded incremental OAA grant push"),z(j.isIncremental===!0,"the workload grant uses an incremental payload");let D=y(Y0(j.payload)?.local_users).find((F)=>F.unique_id==="ecv-reporting-workload");z(D?.custom_properties?.identity_type==="workload","the incremental local_user carries identity_type=workload"),await Q().oaa.clear(),await H.authz.oaa.sync({users:[{id:"ecv-reporting-workload",userName:"ecv-reporting-workload",tenantId:"ecv-preserved-tenant"}],access:W});let P=(await Q().oaa.pushes()).at(-1);z(P!==void 0,"expected a recorded OAA sync push");let B=y(Y0(P.payload)?.local_users).find((F)=>F.unique_id==="ecv-reporting-workload");z(B!==void 0,"the stored workload grant projects its subject as a local_user");let q=B?.custom_properties??{};z(q.identity_type==="workload","the local_user carries identity_type=workload"),z(q.tenantId==="ecv-preserved-tenant","the workload stamp preserves existing local_user custom properties"),await A(Z,G,Y,{subject:"ecv-reporting-workload",subjectType:"user",targetType:"group",target:"region_north_librarian"});let L=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/region_north_librarian",{headers:{Authorization:`Bearer ${K}`}}));z(L.status===200,`requestable Group read should succeed, got ${L.status}`);let O=await L.json(),M=O[W0]??{};z(M.requestable===!0,"the SCIM group is requestable"),z(O.members?.some((F)=>F.value==="ecv-reporting-workload"&&F.type==="Workload")===!0,"the stored workload grant appears as a SCIM member with type Workload"),z(O.members?.some((F)=>F.value==="ecv-reporting-workload"&&F.type==="User")===!0,"a same-id user grant remains a distinct SCIM member")}finally{H.close?.()}}},{name:"incremental group push is referentially closed (conferred role + its permission definitions ride along)",fn:async()=>{await Q().oaa.clear();let K=new Z0;await K.permissions.set({id:"ecv-status",name:"System Status Checks",description:"status"}).ready(),await K.roles.set({id:"ecv-admin",name:"Admin",description:"admin",permissions:["ecv-status"]}).ready();let Y={id:"ecv-supers",name:"Super Admins",description:"closure test",roles:["ecv-admin"]};await K.groups.set(Y).ready(),await Q().oaa.clear(),await w2(Q(),$,K,async(j)=>{await j.authz.oaa.pushGroup(Y)});let Z=(await Q().oaa.pushes()).at(-1);z(Z!==void 0,"expected a recorded OAA group push");let G=Y0(Z.payload),W=y(G?.local_roles);z(W.some((j)=>j.unique_id==="ecv-admin"),"the conferred role definition must ride along so the reference is not dangling");let J=y(Z.payload.permissions).find((j)=>j.unique_id==="ecv-status");z(J!==void 0,"the permission the conferred role confers must also be included"),z(J?.name==="System Status Checks","the closed-over permission definition carries its display name (not just an id)")}},{name:"full sync nests Resource.resources as OAA sub_resources (team owns project)",fn:async()=>{await Q().oaa.clear();let K={id:"team:acme",type:"team",name:"Acme",description:"Acme team",resources:["project:phoenix"]},Y={id:"project:phoenix",type:"project",name:"Phoenix",description:"Phoenix project",parent:"team:acme"};await l0(Q(),$,async(j)=>{await j.authz.oaa.sync({resources:[K,Y]})});let Z=(await Q().oaa.pushes()).at(-1);z(Z!==void 0,"expected a recorded OAA sync push"),z(Z.isIncremental===!1,"full sync must not be marked incremental");let G=Y0(Z.payload),W=y(G?.resources);z(W.some((j)=>j.id==="team:acme"),"the team resource is a top-level OAA resource"),z(!W.some((j)=>j.id==="project:phoenix"),"the child project must not also appear as a top-level sibling");let H=W.find((j)=>j.id==="team:acme"),J=y(H?.sub_resources);z(J.some((j)=>j.id==="project:phoenix"),"the project nests under the team as sub_resources")}},{name:"incremental pushResource re-emits the top-level ancestor subtree with nested sub_resources",fn:async()=>{await Q().oaa.clear();let K=new Z0,Y=new F2,Z={id:"team:acme",type:"team",name:"Acme",description:"Acme team",resources:["project:phoenix"]},G={id:"project:phoenix",type:"project",name:"Phoenix",description:"Phoenix project",parent:"team:acme"};Y.set(Z),Y.set(G),await Q().oaa.clear(),await w2(Q(),$,K,async(P)=>{await P.authz.oaa.pushResource(G)},Y);let W=(await Q().oaa.pushes()).at(-1);z(W!==void 0,"expected a recorded OAA resource push"),z(W.isIncremental===!0,"pushResource is an incremental push");let H=Y0(W.payload),J=y(H?.resources);z(J.some((P)=>P.id==="team:acme"),"incremental push walks parent to the top-level ancestor"),z(!J.some((P)=>P.id==="project:phoenix"),"the child is nested, not duplicated at top level");let j=J.find((P)=>P.id==="team:acme"),D=y(j?.sub_resources);z(D.some((P)=>P.id==="project:phoenix"),"the pushed project appears under the ancestor as sub_resources")}},{name:"ion.authz.oaa entity deletes emit incremental delete operations for each entity kind",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(Z)=>{await Z.authz.oaa.deleteUser("ecv-del-user"),await Z.authz.oaa.deleteGroup("ecv-del-group"),await Z.authz.oaa.deleteRole("ecv-del-role"),await Z.authz.oaa.deletePermission("ecv-del-perm"),await Z.authz.oaa.deleteResource("ecv-del-resource")});let K=await Q().oaa.pushes();z(K.length>=5,"each entity delete should produce a push");let Y=(Z,G,W)=>K.some((H)=>{let J=Y0(H.payload)??{};return Z(J,H.payload).some((j)=>j[G]===W&&j.operation==="delete")});z(Y((Z)=>y(Z.local_users),"unique_id","ecv-del-user"),"deleteUser marks the user with operation:delete"),z(Y((Z)=>y(Z.local_groups),"unique_id","ecv-del-group"),"deleteGroup marks the group with operation:delete"),z(Y((Z)=>y(Z.local_roles),"unique_id","ecv-del-role"),"deleteRole marks the role with operation:delete"),z(Y((Z,G)=>y(G.permissions),"unique_id","ecv-del-perm"),"deletePermission marks the permission with operation:delete"),z(Y((Z)=>y(Z.resources),"id","ecv-del-resource"),"deleteResource marks the resource with operation:delete")}},{name:"OAA full-sync endpoint returns 404 when RemoteConfig.authz.oaa.fullSync is disabled",fn:async()=>{let K=_(C1(Q(),!1),{validators:R(),stores:{catalogStore:new Z0}});try{await K.ready($);let Y=await K.handler(new Request(O1,{method:"POST"}));z(Y.status===404,`disabled full-sync endpoint should 404, got ${Y.status}`)}finally{K.close?.()}}},{name:"OAA full-sync endpoint requires workload auth when enabled (401 without a token)",fn:async()=>{let K=_(C1(Q(),!0),{validators:R(),stores:{catalogStore:new Z0}});try{await K.ready($);let Y=await K.handler(new Request(O1,{method:"POST"}));z(Y.status!==404,"an enabled endpoint must not 404"),z(Y.status!==200,"an unauthenticated caller must not be allowed to trigger a sync"),z(Y.status===401||Y.status===403,`unauthenticated full-sync should be denied (401/403), got ${Y.status}`)}finally{K.close?.()}}},{name:"OAA full-sync endpoint runs a snapshot for an authorized workload and returns 405 for non-POST",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,j2.id),Y=_(C1(Q(),!0),{validators:R(),stores:{catalogStore:new Z0}});try{await Y.ready($);let Z=await Y.handler(new Request(O1,{method:"GET"}));z(Z.status===405,`full-sync endpoint is POST-only, got ${Z.status} for GET`);let G=await Y.handler(new Request(O1,{method:"POST",headers:{Authorization:`Bearer ${K}`}}));z(G.status===200,`authorized full-sync should succeed, got ${G.status}`);let W=await G.json();z(W.reconcile!==void 0&&typeof W.reconcile.subjects==="number"&&typeof W.reconcile.roots==="number"&&typeof W.reconcile.cleared==="number","the endpoint bundles a full-reconcile report alongside the OAA sync result");let H=(await Q().oaa.pushes()).at(-1);z(H!==void 0,"the endpoint should drive an OAA sync push"),z(H.isIncremental===!1,"the endpoint runs a full (snapshot) sync, not an incremental push")}finally{Y.close?.()}}},{name:"routing.resolve replacing iam.users inherits the requiredPermission gate unless it opts out with null (§16.1)",fn:async()=>{let K=await E(),Y="custom-iam-users-handler",Z=_(h({id:"ecv-resolve-inherit",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:K.catalog,accessStore:new v,accessIndex:new U,resources:K.resources},routing:{resolve:(G,W,H)=>{if(!H||new URL(G.url).pathname!=="/api/ionite/iam/Users")return H;let J={module:H.module,kind:"iam.users.custom",handler:async()=>new Response("custom-iam-users-handler",{status:200})};if(G.headers.get("x-ion-resolve-mode")==="public")return{...J,requiredPermission:null};return J}}});try{await Z.ready($);let G=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Users",{headers:{"x-ion-resolve-mode":"inherit"}}));z(G.status===401||G.status===403,`a replaced route omitting requiredPermission must inherit the gate (401/403), got ${G.status}`),z(await G.text()!=="custom-iam-users-handler","the custom handler must not run when the inherited gate denies");let W=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Users",{headers:{"x-ion-resolve-mode":"public"}}));z(W.status===200,`requiredPermission: null must reach the handler, got ${W.status}`),z(await W.text()==="custom-iam-users-handler","the explicit public opt-out reaches the custom handler");let H=await Z.handler(new Request("https://ecv.test/api/ionite/does-not-exist"));z(H.status===404,`an unmatched route returns 404, got ${H.status}`);let J=await H.json();w(J.error,"not_found",'the router 404 uses the unified JSON shape { error: "not_found" }')}finally{Z.close?.()}}},{name:"a failed SCIM Group membership mutation is auto-repaired by the SDK (awaited subject reconcile within the request, no IDP retry)",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=await E(),Z=new v,G=new H5,W=_(h({id:"ecv-scim-repair",workload:{jwksUri:`${Q().baseUrl}/workload/certs`,audience:`${Q().baseUrl}/workload`},authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:Z,accessIndex:G,resources:Y.resources}});try{await W.ready($);let H=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"repair-user"}]}]})}));z(H.status>=400,`a failed access mutation should return a SCIM error, got ${H.status}`),z((await Z.listBySubject("repair-user","user")).items.length===1,"the grant root is durably committed even though the index write failed"),z((await G.hasGroup("repair-user","staff",void 0,"user")).allow,"the SDK awaited-repair re-materializes the subject index within the request (no IDP retry)"),z((await G.hasPermission("repair-user","read",void 0,"user")).allow,"the repaired closure includes the role/permission edges (editor -> read via staff)")}finally{W.close?.()}}},{name:"settlement: onAccessMutation fires exactly once with summary.egress on every exit path",fn:async()=>{let K=Q().baseUrl,Y=await S(K,V0.id),Z=await S(K,x$.id),G={jwksUri:`${K}/workload/certs`,audience:`${K}/workload`},W=(H,J)=>new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${J}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:H}]}]})});{let H=await E(),J=[],j=_(h({id:"settle-success",workload:G,authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),authz:{onAccessMutation:(D)=>J.push(D)},stores:{catalogStore:H.catalog,accessStore:new v,accessIndex:new U,resources:H.resources}});try{await j.ready($);let D=await j.handler(W("settle-ok",Y));z(D.status>=200&&D.status<300,`success exit should be 2xx, got ${D.status}`),w(J.length,1,"onAccessMutation fires exactly once on the success exit"),z(J[0]?.egress!==void 0,"summary.egress is populated on the success exit"),z(J[0]?.access.some((P)=>P.status==="applied"),"success summary records the applied grant")}finally{j.close?.()}}{let H=await E(),J=[],j=_(h({id:"settle-access-fail",workload:G,authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),authz:{onAccessMutation:(D)=>J.push(D)},stores:{catalogStore:H.catalog,accessStore:new v,accessIndex:new H5,resources:H.resources}});try{await j.ready($);let D=await j.handler(W("settle-fail",Y));z(D.status>=400,`access-failure exit should return a SCIM error, got ${D.status}`),w(J.length,1,"onAccessMutation fires exactly once on the access-failure exit"),z(J[0]?.egress!==void 0,"summary.egress is populated on the access-failure exit"),z(J[0]?.access.some((P)=>P.status==="failed"),"access-failure summary records the failed access mutation")}finally{j.close?.()}}{let H=[],J=_(h({id:"settle-metadata-fail",workload:G,authz:{scopesAsEntitlements:!0},iam:{}}),{validators:R(),authz:{onAccessMutation:(j)=>H.push(j)},stores:{userStore:new R$}});try{await J.ready($);let j=await J.handler(new Request("https://ecv.test/api/ionite/iam/Users",{method:"POST",headers:{Authorization:`Bearer ${Z}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],userName:"settle-metadata-fail@example.com",externalId:"settle-metadata-fail",name:{givenName:"Settle",familyName:"Fail"},emails:[{value:"settle-metadata-fail@example.com",primary:!0}]})}));z(j.status>=400,`metadata-failure exit should return a SCIM error, got ${j.status}`),w(H.length,1,"onAccessMutation fires exactly once on the metadata-failure exit"),z(H[0]?.egress!==void 0,"summary.egress is populated on the metadata-failure exit"),z(H[0]?.metadataApplied===!1,"metadata-failure summary marks metadataApplied false")}finally{J.close?.()}}}},{name:"settlement: a degraded OAA push is non-fatal (SCIM stays 2xx, summary.egress.oaa is degraded)",fn:async()=>{let K=Q(),Y=K.baseUrl,Z=await S(Y,V0.id),G=await E(),W=[],H=_(h({id:"settle-oaa-degraded",workload:{jwksUri:`${Y}/workload/certs`,audience:`${Y}/workload`},authz:{scopesAsEntitlements:!0,oaa:{url:`${Y}/__oaa_missing__`,apiKey:K.oaa.apiKey,providerId:K.oaa.providerId,datasourceId:K.oaa.datasourceId,applicationName:"ECV Settlement Degraded"}},iam:{}}),{validators:R(),authz:{onAccessMutation:(J)=>W.push(J)},stores:{catalogStore:G.catalog,accessStore:new v,accessIndex:new U,resources:G.resources}});try{await H.ready($),z(H.authz.oaa.isReady(),"OAA must be configured/ready so the push is actually attempted");let J=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${Z}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"oaa-degraded-member"}]}]})}));z(J.status>=200&&J.status<300,`a degraded OAA push must not fail the SCIM response, got ${J.status}`),w(W.length,1,"onAccessMutation fires exactly once"),w(W[0]?.egress?.oaa.status,"degraded","the failed OAA push is recorded as degraded egress"),z(W[0]?.access.some((j)=>j.status==="applied"),"the underlying store mutation still committed (applied)")}finally{H.close?.()}}},{name:"SCIM Groups exposes only requestable app rows across list/get/externalId and write paths",fn:async()=>{let K=await S(Q().baseUrl,H1.id),Y=await S(Q().baseUrl,V0.id),Z=await E();await Z.catalog.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,roles:["editor"],resources:["team:acme"],source:"app"}).ready();let G=A1(),W=_(e0(Q().baseUrl,"ecv-app-row-read"),{validators:R(),stores:{catalogStore:Z.catalog,accessStore:new v,accessIndex:new U,resources:G}});try{await W.ready($);let H={Authorization:`Bearer ${K}`},J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{headers:H}));z(J.status===200,`Groups list should succeed, got ${J.status}`);let j=await J.json(),D=(j.Resources??[]).map((C)=>C.id);z(D.includes("acme_team_admin"),"the durable app-authored entitlement is listed"),z(!D.includes("internal_staff"),"the non-requestable stored group is hidden"),z(j.totalResults===D.length,"totalResults counts only the requestable IAM surface");let q=((await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq true")}`,{headers:H}))).json()).Resources??[]).map((C)=>C.id);z(q.includes("acme_team_admin"),"the app entitlement passes the requestable filter"),z(!q.includes("internal_staff"),"the non-requestable stored group is excluded");let O=await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq false")}`,{headers:H}))).json();z(O.totalResults===0&&(O.Resources??[]).length===0,"requestable eq false cannot reveal internal definitions");let F=await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "iam-acme-team-admin"')}`,{headers:H}))).json();z((F.Resources??[]).some((C)=>C.id==="acme_team_admin"),"externalId filtering matches the row by providerId");let x=await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "iam-internal-staff"')}`,{headers:H}))).json();z(x.totalResults===0&&(x.Resources??[]).length===0,"externalId lookup cannot reveal a non-requestable row");let b=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{headers:H}));z(b.status===200,`app Group GET should succeed, got ${b.status}`);let p=await b.json();z(p.id==="acme_team_admin"&&p.displayName==="Acme Admin","durable presentation is projected"),z(p[W0]?.source==="app","the SCIM projection carries source:app provenance"),z(p[W0]?.resources?.[0]==="team:acme","the SCIM projection carries the durable resource binding");let F0=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/internal_staff",{headers:H}));z(F0.status===404,`non-requestable point GET must be 404, got ${F0.status}`);let k=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/internal_staff",{method:"PATCH",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"must-not-grant"}]}]})}));z(k.status===404,`IAM membership write on an internal entitlement must be 404, got ${k.status}`)}finally{W.close?.()}}},{name:"a member grant on an app catalog row materializes resource-scoped access and IAM receipt write-back is accepted",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=await E();await Y.catalog.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,roles:["editor"],resources:["team:acme"],source:"app"}).ready();let Z=A1(),G=new v,W=new U,H=_(e0(Q().baseUrl,"ecv-app-row-write"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:G,accessIndex:W,resources:Z}});try{await H.ready($);let J={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},j=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"derived-member",type:"User"},{value:"receipt-workload",type:"Workload"}]}]})}));z(j.status<300,`member add on an app entitlement should succeed, got ${j.status}`),z((await W.hasRole("derived-member","editor","team:acme","user")).allow,"the grant confers the app-authored role scoped to the backing resource"),z(!(await W.hasRole("derived-member","editor","region:north","user")).allow,"the app-authored scope does not leak to unrelated resources");let D=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.providerId`,value:"iam-group-42"},{op:"replace",path:`${W0}.requestUrl`,value:"https://iam.example.test/request/42"}]})}));z(D.status<300,`IAM receipt PATCH should succeed, got ${D.status}`);let P=await Y.catalog.groups.get("acme_team_admin");z(P?.providerId==="iam-group-42","IAM providerId is written back to the app row"),z(P?.requestUrl==="https://iam.example.test/request/42","IAM requestUrl is written back to the app row")}finally{H.close?.()}}},{name:"code templates union publicly, materialize on SCIM discovery, and preserve IAM-owned presentation",fn:async()=>{let K=await S(Q().baseUrl,H1.id),Y=await S(Q().baseUrl,V0.id),Z=await E(),G=new U;await Z.catalog.groups.set({id:"code_staff",name:"IAM Renamed Staff",displayName:"IAM Renamed Staff",description:"Presentation owned by IAM",requestable:!1,source:"code",providerId:"provider-code-staff",requestUrl:"https://iam.example.test/request/code-staff",roles:["stored-role-must-not-control-structure"]}).ready();let W=_(e0(Q().baseUrl,"ecv-code-read"),{validators:R(),stores:{catalog:J1(),catalogStore:Z.catalog,accessStore:new v,accessIndex:G}});try{await W.ready($);let H={Authorization:`Bearer ${K}`},j=(await W.authz.entitlements.list()).items.map((k)=>k.id);z(j.includes("code_staff"),"the public API includes a code id with a materialized row"),z(j.includes("code_editor"),"the public API includes unmaterialized code templates"),z(j.includes("staff"),"the public API unions app/store rows"),z(await Z.catalog.roles.get("code_editor")===void 0,"public union reads do not themselves materialize code templates");let D=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{headers:H}));z(D.status===200,`Groups list should succeed, got ${D.status}`);let P=await D.json(),B=(P.Resources??[]).map((k)=>k.id);z(B.includes("code_staff"),"the code-declared group is listed"),z(!B.includes("code_editor"),"the non-requestable code role is hidden from IAM"),z(B.includes("staff"),"the requestable store group remains visible to IAM"),z(!B.includes("internal_staff"),"the non-requestable store group is hidden from IAM"),z(P.totalResults===B.length,"totalResults counts only requestable entries"),z((await Z.catalog.groups.get("code_staff"))?.source==="code","SCIM Group list lazily materializes requestable code templates"),z(await Z.catalog.roles.get("code_editor")===void 0&&await Z.catalog.permissions.get("code_read")===void 0,"SCIM Group list does not materialize internal code templates");let O=((await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq true")}`,{headers:H}))).json()).Resources??[]).map((k)=>k.id);z(O.includes("code_staff"),"the code template requestability wins over the stale stored presentation"),z(O.includes("staff"),"the requestable store group remains included"),z(!O.includes("internal_staff"),"the non-requestable store group is excluded");let T=((await(await W.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "code_editor"')}`,{headers:H}))).json()).Resources??[]).find((k)=>k.id==="code_editor");z(T===void 0,"externalId filtering hides the internal code entitlement"),z(await Z.catalog.roles.get("code_editor")===void 0,"hidden externalId lookup does not materialize the internal code entitlement");let x=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{headers:H}));z(x.status===200,`code Group GET should succeed, got ${x.status}`);let b=await x.json();z(b.id==="code_staff"&&b.externalId==="provider-code-staff","the App id and IAM provider id are projected in their SCIM id/externalId slots"),z(b.displayName==="IAM Renamed Staff","the existing row owns presentation"),z(b[W0]?.requestable===!0,"code-owned requestability overrides the stale stored value"),z(b[W0]?.providerId==="provider-code-staff"&&b[W0]?.requestUrl==="https://iam.example.test/request/code-staff","the existing row owns IAM receipts");let p=await Z.catalog.groups.get("code_staff");z(p?.displayName==="IAM Renamed Staff"&&p.roles?.[0]==="stored-role-must-not-control-structure","insert-if-absent discovery never overwrites an existing durable row");let F0=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"iam-presentation-member"}]}]})}));z(F0.status<300,`membership grant should succeed, got ${F0.status}`),z((await G.hasRole("iam-presentation-member","code_editor",void 0,"user")).allow,"code structural references remain authoritative over conflicting stored structure"),z(!(await G.hasRole("iam-presentation-member","stored-role-must-not-control-structure",void 0,"user")).allow,"the IAM-owned row cannot replace code-owned graph structure")}finally{W.close?.()}}},{name:"first code membership grant materializes durably; IAM presentation writes succeed while structure stays code-owned",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new v,Z=new U,G=new Z0,W=_(e0(Q().baseUrl,"ecv-code-write"),{validators:R(),stores:{catalog:J1(),catalogStore:G,accessStore:Y,accessIndex:Z}});try{await W.ready($);let H={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"};z(await G.groups.get("code_staff")===void 0,"the code template begins unmaterialized");let J=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"code-member"}]}]})}));z(J.status<300,`member add on a code entitlement should succeed, got ${J.status}`);let j=await G.groups.get("code_staff");z(j?.source==="code","the first membership grant materializes source:code"),z(j?.roles?.[0]==="code_editor","the durable candidate initially carries code structure"),z((await Z.hasRole("code-member","code_editor",void 0,"user")).allow,"the grant confers the code role"),z((await Z.hasPermission("code-member","code_read",void 0,"user")).allow,"the code role confers its permissions");let D=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"remove",path:'members[value eq "code-member"]'}]})}));z(D.status<300,`member remove should succeed, got ${D.status}`),z(!(await Z.hasGroup("code-member","code_staff",void 0,"user")).allow,"the revoke converges the closure");let P=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:"IAM Renamed Code Staff"},{op:"replace",path:`${W0}.providerId`,value:"iam-code-staff"},{op:"replace",path:`${W0}.requestUrl`,value:"https://iam.example.test/request/code-staff"}]})}));z(P.status<300,`code presentation/receipt PATCH should succeed, got ${P.status}`);let B=await G.groups.get("code_staff");z(B?.displayName==="IAM Renamed Code Staff","IAM presentation persists on the row"),z(B?.providerId==="iam-code-staff"&&B.requestUrl==="https://iam.example.test/request/code-staff","IAM receipt fields persist on the row"),z(B?.roles?.[0]==="code_editor","presentation write-back does not erase code structure");let q=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.requestable`,value:!1}]})}));z(q.status===403,`code-owned requestability PATCH must be 403, got ${q.status}`);let L=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.resources`,value:["team:hijack"]}]})}));z(L.status===403,`code structure PATCH must be 403, got ${L.status}`);let O=await L.json();z(O.scimType==="mutability","the rejection is a SCIM mutability error");let M=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PUT",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],id:"code_staff",externalId:"iam-code-staff-v2",displayName:"Code Staff"})}));z(M.status<300,`provider externalId PUT should succeed, got ${M.status}`),z((await G.groups.get("code_staff"))?.providerId==="iam-code-staff-v2","the inbound client externalId is stored as providerId without changing the App-owned catalog id");let F=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"DELETE",headers:H}));z(F.status===403,`active code Group DELETE must be 403, got ${F.status}`)}finally{W.close?.()}}},{name:"SCIM create stores the client externalId as providerId without claiming the App id",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0,Z=_(e0(Q().baseUrl,"ecv-code-collision"),{validators:R(),stores:{catalog:J1(),catalogStore:Y,accessStore:new v,accessIndex:new U}});try{await Z.ready($);let G=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"Impostor",externalId:"code_staff"})}));z(G.status===201,`a client externalId create must succeed, got ${G.status}`);let W=await G.json();z(W.id!=="code_staff","the App mints a distinct catalog id"),z(W.externalId==="code_staff","the response echoes the client-owned externalId"),z(W.id!==void 0&&(await Y.groups.get(W.id))?.providerId==="code_staff","the client externalId is stored as providerId")}finally{Z.close?.()}}},{name:"SCIM Group create defaults to requestable and rejects explicitly internal definitions",fn:async()=>{let K=await S(Q().baseUrl,V0.id),Y=new Z0,Z=_(e0(Q().baseUrl,"ecv-requestable-create"),{validators:R(),stores:{catalogStore:Y,accessStore:new v,accessIndex:new U}});try{await Z.ready($);let G={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},W=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:G,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],externalId:"iam-authored-requestable",displayName:"IAM Authored Requestable"})}));z(W.status===201,`plain SCIM Group create should succeed, got ${W.status}`);let H=await W.json();z(H[W0]?.requestable===!0,"a SCIM-authored Group defaults to requestable"),z((await Y.groups.get(H.id??""))?.requestable===!0,"the requestable default is durable");let J=await Z.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:G,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group",W0],externalId:"iam-authored-internal",displayName:"IAM Authored Internal",[W0]:{requestable:!1}})}));z(J.status===400,`IAM cannot create an internal entitlement, got ${J.status}`)}finally{Z.close?.()}}},{name:"full reconcile deprecates retired code rows; SCIM exposes retirement and only then permits deletion",fn:async()=>{let K=await S(Q().baseUrl,H1.id),Y=await S(Q().baseUrl,V0.id),Z=new Z0;await Z.groups.set({id:"retired_code_group",providerId:"iam-retired-code-group",name:"Retired Code Group",description:"No longer present in the current CodeCatalog",requestable:!0,source:"code",deprecated:!1}).ready(),await Z.groups.set({id:"pull_retired_code_group",providerId:"iam-pull-retired-code-group",name:"Pull-Retired Code Group",description:"Retired lazily during SCIM discovery",requestable:!0,source:"code",deprecated:!1}).ready();let G=_(e0(Q().baseUrl,"ecv-code-retirement"),{validators:R(),stores:{catalog:J1(),catalogStore:Z,accessStore:new v,accessIndex:new U}});try{await G.ready($);let W=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/pull_retired_code_group",{headers:{Authorization:`Bearer ${K}`}}));z(W.status===200,"SCIM GET should project a retired code row"),z((await Z.groups.get("pull_retired_code_group"))?.deprecated===!0,"SCIM discovery lazily marks a source:code row missing from the current CodeCatalog deprecated");let H=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/pull_retired_code_group",{method:"PATCH",headers:{Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${W0}.requestable`,value:!1}]})}));z(H.status===403,`retired source:code requestability remains immutable, got ${H.status}`);let J=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{method:"DELETE",headers:{Authorization:`Bearer ${Y}`}}));z(J.status===403,"a source:code row cannot be deleted before retirement"),await G.authz.reconcile(),z((await Z.groups.get("retired_code_group"))?.deprecated===!0,"full reconcile marks a source:code row missing from the current CodeCatalog deprecated");let j=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{headers:{Authorization:`Bearer ${K}`}}));z(j.status===200,`deprecated Group GET should succeed, got ${j.status}`);let D=await j.json();z(D[W0]?.source==="code"&&D[W0]?.deprecated===!0,"SCIM projects source:code and deprecated:true");let P=await G.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{method:"DELETE",headers:{Authorization:`Bearer ${Y}`}}));z(P.status===204,`a deprecated code row may be deleted, got ${P.status}`),z(await Z.groups.get("retired_code_group")===void 0,"the retired row is removed")}finally{G.close?.()}}},{name:"OAA full sync unions code templates with durable app rows and resource data",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,j2.id),Y=new Z0;await Y.groups.set({id:"stored-group",name:"Stored Group",description:"stored",source:"app"}).ready(),await Y.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,roles:["code_editor"],resources:["team:acme"],source:"app"}).ready();let Z=_(C1(Q(),!0),{validators:R(),stores:{catalog:J1(),catalogStore:Y,resources:A1()}});try{await Z.ready($);let G=await Z.handler(new Request(O1,{method:"POST",headers:{Authorization:`Bearer ${K}`}}));z(G.status===200,`authorized full-sync should succeed, got ${G.status}`);let W=(await Q().oaa.pushes()).at(-1);z(W!==void 0&&W.isIncremental===!1,"the endpoint runs a full (snapshot) sync");let H=Y0(W.payload),J=y(H?.local_groups).map((P)=>P.unique_id);z(J.includes("code_staff"),"code-declared groups are unioned into the projection"),z(J.includes("stored-group"),"store catalog groups remain in the projection"),z(J.includes("acme_team_admin"),"durable resource-scoped app rows are projected");let j=y(H?.local_roles).map((P)=>P.unique_id);z(j.includes("code_editor"),"code-declared roles are unioned into the projection");let D=y(W.payload.permissions).map((P)=>P.unique_id);z(D.includes("code_read"),"code-declared permissions are unioned into the projection")}finally{Z.close?.()}}},{name:"ResourceProvider.changed triggers an off-request-path reconcile and an incremental OAA push",fn:async()=>{await Q().oaa.clear();let K=await E(),Y=new v,Z=new U;K.resources.set({id:"team:a",type:"team",name:"Team A",description:"A team"}),await A(Y,Z,K,{subject:"lead",subjectType:"user",targetType:"permission",target:"manage",resources:["team:a"]});let G=_(L2(Q()),{validators:R(),stores:{catalogStore:K.catalog,accessStore:Y,accessIndex:Z,resources:K.resources}});try{await G.ready($),z(!(await Z.hasPermission("lead","manage","proj:a1","user")).allow,"no child access before the change"),await Q().oaa.clear(),K.resources.set({id:"proj:a1",type:"project",name:"Project A1",description:"A1",parent:"team:a"});let W=!1;for(let J=0;J<100&&!W;J++)await new Promise((j)=>setTimeout(j,10)),W=(await Z.hasPermission("lead","manage","proj:a1","user")).allow;z(W,"the changed signal drives a reconcile that cascades manage to the new child");let H=!1;for(let J=0;J<100&&!H;J++)await new Promise((j)=>setTimeout(j,10)),H=(await Q().oaa.pushes()).some((j)=>{let D=Y0(j.payload),P=(B)=>B.flatMap((q)=>[q,...P(y(q.sub_resources))]);return j.isIncremental===!0&&P(y(D?.resources)).some((B)=>B.id==="proj:a1")});z(H,"the changed signal drives an incremental OAA resource push")}finally{G.close?.()}}},{name:"OAA full sync includes ResourceProvider resources and durable app-authored catalog rows",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,j2.id),Y=new Z0;await Y.groups.set({id:"stored-group",name:"Stored Group",description:"stored",source:"app"}).ready(),await Y.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,resources:["team:acme"],source:"app"}).ready();let Z=_(C1(Q(),!0),{validators:R(),stores:{catalogStore:Y,resources:A1()}});try{await Z.ready($);let G=await Z.handler(new Request(O1,{method:"POST",headers:{Authorization:`Bearer ${K}`}}));z(G.status===200,`authorized full-sync should succeed, got ${G.status}`);let W=(await Q().oaa.pushes()).at(-1);z(W!==void 0,"the endpoint should drive an OAA sync push"),z(W.isIncremental===!1,"the endpoint runs a full (snapshot) sync");let H=Y0(W.payload);z(y(H?.resources).some((j)=>j.id==="team:acme"),"the snapshot streams resources from the ResourceProvider");let J=y(H?.local_groups).map((j)=>j.unique_id);z(J.includes("acme_team_admin"),"durable app catalog rows are included in the projection"),z(J.includes("stored-group"),"stored catalog groups remain in the projection")}finally{Z.close?.()}}},{name:"the SDK write path (SCIM access mutation + ResourceProvider.changed) emits only incremental OAA pushes, never a full snapshot",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,V0.id),Y=await E(),Z=A1(),G=new v,W=new U,H=_(D2(Q(),"ecv-scim-oaa-guard"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:G,accessIndex:W,resources:Z}});try{await H.ready($),z(H.authz.oaa.isReady(),"OAA must be ready so the write path actually projects");let J={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"};await Y.catalog.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],resources:["team:acme"],source:"app"}).ready();let j=async()=>(await Q().oaa.pushes()).every((L)=>L.isIncremental===!0),D=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"guard-user-1",type:"User"}]}]})}));z(D.status<300,`stored group member add should succeed, got ${D.status}`);let P=await H.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:J,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"guard-user-2",type:"User"}]}]})}));z(P.status<300,`app group member add should succeed, got ${P.status}`),z((await Q().oaa.pushes()).length>=2,"each SCIM membership grant should project an incremental push"),z((await Q().oaa.pushes()).some((L)=>{let O=Y0(L.payload);return L.isIncremental===!0&&y(O?.local_groups).some((M)=>M.unique_id==="acme_team_admin")}),"the durable app catalog write projects the Group incrementally"),z(await j(),"a SCIM access mutation must not trigger a full OAA snapshot sync"),Z.set({id:"team:beta",type:"team",name:"Beta",description:"Beta team"});let B=!1;for(let L=0;L<100&&!B;L++)await new Promise((O)=>setTimeout(O,10)),B=(await Q().oaa.pushes()).some((O)=>{let M=Y0(O.payload);return O.isIncremental===!0&&y(M?.resources).some((F)=>F.id==="team:beta")});z(B,"a resource change should project the resource to OAA incrementally"),await new Promise((L)=>setTimeout(L,50));let q=(await Q().oaa.pushes()).filter((L)=>L.isIncremental===!1);z(q.length===0,`the SDK write path emitted ${q.length} full snapshot push(es); full sync must only run via the Ops-gated endpoint`)}finally{H.close?.()}}},{name:"§7.1 pushAccessChanges batches N membership changes into ONE incremental push (O(1), not O(N))",fn:async()=>{await Q().oaa.clear();let K=Array.from({length:100},(J,j)=>({change:{subject:`batch-user-${j}`,subjectType:"user",targetType:"group",target:"batch-group"},op:"add"})),Y=new Z0;await Y.groups.set({id:"batch-group",name:"Batch Group",description:"Batch projection target"}).ready(),await w2(Q(),$,Y,async(J)=>{await J.authz.oaa.pushAccessChanges(K)});let Z=await Q().oaa.pushes();w(Z.length,1,"a 100-change batch must issue exactly one OAA push, not one per change");let G=Z[0];z(G!==void 0,"expected the single batched push to be recorded"),z(G?.isIncremental===!0,"a batched membership push is incremental, never a full snapshot");let W=y(Y0(G?.payload??{})?.local_users);for(let J=0;J<100;J+=1){let j=W.find((D)=>D.unique_id===`batch-user-${J}`);z(j!==void 0,`subject batch-user-${J} must appear in the single batched payload`),z(Array.isArray(j?.groups)&&(j?.groups).includes("batch-group"),`subject batch-user-${J} must carry the granted group membership`)}let H=W.find((J)=>J.unique_id==="batch-user-0");z(H?.operation==="add","a batched add projects each membership with the add operation")}},{name:"§7.1 pushAccessChanges([]) is a no-op — an empty batch never contacts the OAA receiver",fn:async()=>{await Q().oaa.clear(),await l0(Q(),$,async(K)=>{await K.authz.oaa.pushAccessChanges([])}),w((await Q().oaa.pushes()).length,0,"an empty change batch must not issue any OAA push")}},{name:"re-pushing the SAME SCIM membership grant keeps IAM entitlement members and OAA edges stable (idempotent, no accumulation)",fn:async()=>{await Q().oaa.clear();let K=await S(Q().baseUrl,`${V0.id} ${H1.id}`),Y=await E(),Z=new v,G=new U,W=_(D2(Q(),"ecv-scim-oaa-idempotent"),{validators:R(),stores:{catalogStore:Y.catalog,accessStore:Z,accessIndex:G}});try{await W.ready($),z(W.authz.oaa.isReady(),"OAA must be ready so the write path projects each grant");let H={Authorization:`Bearer ${K}`,"Content-Type":"application/scim+json"},J=()=>new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:H,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"idem-user"}]}]})}),j=async()=>{let p=new Set;for(let F0 of await Q().oaa.pushes())for(let k of y(Y0(F0.payload)?.local_users)){if(k.operation==="delete")continue;let C=typeof k.unique_id==="string"?k.unique_id:"";for(let X0 of Array.isArray(k.groups)?k.groups:[])if(typeof X0==="string")p.add(`${C}:member_of:group:${X0}`)}return p},D=await W.handler(J());z(D.status<300,`first membership grant should succeed, got ${D.status}`);let P=(await Z.listByTarget("group","staff")).items.length,B=await j(),q=(await Q().oaa.pushes()).length;z(P===1,"the first grant persists exactly one membership root"),z(B.has("idem-user:member_of:group:staff"),"the first grant projects the membership edge"),z((await G.hasPermission("idem-user","read",void 0,"user")).allow,"the granted staff membership confers the editor role closure (read)");let L=await W.handler(J());z(L.status<300,`re-granting the same membership should still succeed, got ${L.status}`);let O=(await Z.listByTarget("group","staff")).items.length,M=await j(),F=(await Q().oaa.pushes()).length;z(O===P,`re-pushing the same grant must not add IAM entitlement roots (was ${P}, now ${O})`),z(M.size===B.size,`re-pushing the same grant must not add OAA edges (was ${B.size}, now ${M.size})`);for(let p of B)z(M.has(p),`OAA edge ${p} must remain stable across an identical re-push`);z(F>q,"the SDK re-projects on the second grant (the receiver, not the SDK, dedupes the edge)");let T=await W.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{headers:{Authorization:`Bearer ${K}`}}));z(T.status===200,`Groups/staff read should succeed, got ${T.status}`);let b=((await T.json()).members??[]).filter((p)=>p.value==="idem-user");z(b.length===1,`the re-granted member must appear exactly once in the SCIM projection, saw ${b.length}`)}finally{W.close?.()}}}]}function A1(){let X=new F2;return X.set({id:"team:acme",type:"team",name:"Acme",description:"Acme team"}),X}function e0(X,$){return h({id:$,workload:{jwksUri:`${X}/workload/certs`,audience:`${X}/workload`},authz:{scopesAsEntitlements:!0},iam:{}})}async function S(X,$){let Q=new URLSearchParams({grant_type:"client_credentials",client_id:"local-workload-client",client_secret:"local-workload-secret",scope:$}),Y=await(await fetch(`${X}/workload/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:Q})).json();return z(typeof Y.access_token==="string","mock workload token endpoint must return an access_token"),Y.access_token}function S1(X,$){return _(h({id:"ecv-workload-scope",workload:{jwksUri:`${X}/workload/certs`,audience:`${X}/workload`},...$?{authz:$}:{}}),{validators:R()})}function M$(X,$,Q){return _(h({id:"ecv-workload-incoming",workload:{incoming:$},...Q?{authz:Q}:{}}),{validators:R()})}function s0(X){return new Request("https://example.test/api/task",{headers:{Authorization:`Bearer ${X}`}})}async function B2(X,$,Q){try{await X()}catch(K){let Y=K instanceof Error?K.message:String(K);if(!Y.includes($))throw Error(`${Q}: expected error containing "${$}", got "${Y}"`);return}throw Error(`${Q}: expected rejection, but the call resolved`)}function U$(X){let $=X.timeout??5000,Q=()=>v$(X.ecv);return[{name:"hasScope* authorize a workload from its token scope claim (allow / deny + 403 mapping)",fn:async()=>{let K=await S(Q().baseUrl,"oaa:sync reports:read"),Y=S1(Q().baseUrl);try{await Y.ready($);let Z=s0(K),G=await q2(Z,Y,"oaa:sync","workload");z(G.allow,"token scope oaa:sync satisfies hasScope"),z(G.subjectType==="workload","the matched principal is the workload"),z(G.subject?.id==="local-workload-client","workload id is the client_id");let W=await q2(Z,Y,"admin:all","workload");z(!W.allow&&W.reason==="forbidden","a missing scope is forbidden"),z(W.response().status===403,"forbidden scope -> 403"),z((await T$(Z,Y,["nope","reports:read"],"workload")).allow,"hasAnyScope matches one"),z(!(await P2(Z,Y,["oaa:sync","nope"],"workload")).allow,"hasAllScope needs all"),z((await P2(Z,Y,["oaa:sync","reports:read"],"workload")).allow,"hasAllScope all present")}finally{Y.close?.()}}},{name:"scopesAsEntitlements gates whether has* auto-honors a workload token scope (app-wide only)",fn:async()=>{let K=await S(Q().baseUrl,"run_oaa_sync"),Y=s0(K),Z=S1(Q().baseUrl,{scopesAsEntitlements:!1});try{await Z.ready($);let W=await _0(Y,Z,"run_oaa_sync",void 0,"workload");z(!W.allow,"scopes do not satisfy has* when scopesAsEntitlements is false")}finally{Z.close?.()}let G=S1(Q().baseUrl,{scopesAsEntitlements:!0});try{await G.ready($);let W=await _0(Y,G,"run_oaa_sync",void 0,"workload");z(W.allow,"app-wide has* auto-honors the matching token scope when enabled");let H=await _0(Y,G,"run_oaa_sync","project-a","workload");z(!H.allow,"a token scope cannot satisfy a resource-scoped has* check")}finally{G.close?.()}}},{name:"trustedProvisioningClients overlay authorizes a matching workload without a store grant or scope",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,""),G={issuer:Y,audience:[Y],clientId:"local-workload-client",permissions:[k1.id]},W=S1(K,{trustedProvisioningClients:[G]});try{await W.ready($);let H=s0(Z),J=await _0(H,W,k1.id,void 0,"workload");z(J.allow,"trusted client is authorized for its declared permission via the overlay"),z(J.decision?.metadata?.reason==="trusted-client","allow reason is trusted-client");let j=await _0(H,W,"ion:iam:read",void 0,"workload");z(!j.allow,"a permission outside the trusted client declaration is denied")}finally{W.close?.()}}},{name:"trustedProvisioningClients overlay is audience-bound (a mismatched audience does not authorize)",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,""),G={issuer:Y,audience:["https://some-other-app.example/api"],clientId:"local-workload-client",permissions:[k1.id]},W=S1(K,{trustedProvisioningClients:[G]});try{await W.ready($);let H=await _0(s0(Z),W,k1.id,void 0,"workload");z(!H.allow,"audience mismatch means the trusted-client overlay does not apply")}finally{W.close?.()}}},{name:"per-IdP scopesAsEntitlements honors token scope even when the global flag is off",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,"run_oaa_sync"),G=M$(K,[{issuer:Y,jwksUri:`${K}/workload/certs`,audience:Y,scopesAsEntitlements:!0}]);try{await G.ready($);let W=await _0(s0(Z),G,"run_oaa_sync",void 0,"workload");z(W.allow,"the matched IdP scopesAsEntitlements honors the token scope"),z(W.decision?.metadata?.reason==="scope","allow reason is scope")}finally{G.close?.()}}},{name:"multi-IdP incoming enforces audience (cross-app token reuse is rejected at validation)",fn:async()=>{let K=Q().baseUrl,Y=`${K}/workload`,Z=await S(K,"run_oaa_sync"),G=M$(K,[{issuer:Y,jwksUri:`${K}/workload/certs`,audience:"https://other-app.example/api"}],{scopesAsEntitlements:!0});try{await G.ready($);let W=await _0(s0(Z),G,"run_oaa_sync",void 0,"workload");z(!W.allow,"a token whose audience does not match the trusted IdP is rejected"),z(W.reason==="unauthenticated","audience mismatch fails validation -> unauthenticated")}finally{G.close?.()}}},{name:"trustedProvisioningClients without an audience is rejected at config apply (§1.4)",fn:async()=>{let K=Q().baseUrl,Y={issuer:`${K}/workload`,audience:[],clientId:"eci-provisioning",permissions:["ion:iam:read"]},Z=_(h({id:"ecv-1-4-empty-audience",workload:{jwksUri:`${K}/workload/certs`,audience:`${K}/workload`},authz:{trustedProvisioningClients:[Y]}}),{validators:R()});try{await B2(()=>Z.ready($),"audience","an audience-less trusted provisioning client must fail config apply")}finally{Z.close?.()}}},{name:"incoming workload trust without an audience is rejected at config apply (§1.5)",fn:async()=>{let K=Q().baseUrl,Y=_(h({id:"ecv-1-5-flat-no-audience",workload:{jwksUri:`${K}/workload/certs`}}),{validators:R()});try{await B2(()=>Y.ready($),"audience","a server-only workload trust without an audience must fail config apply")}finally{Y.close?.()}let Z=_(h({id:"ecv-1-5-incoming-no-audience",workload:{incoming:[{issuer:`${K}/workload`,jwksUri:`${K}/workload/certs`}]}}),{validators:R()});try{await B2(()=>Z.ready($),"audience","an audience-less TrustedIdp incoming entry must fail config apply")}finally{Z.close?.()}}},{name:"flat server-only workload trust enforces its configured audience at runtime (§1.5)",fn:async()=>{let K=Q().baseUrl,Y=await S(K,"run_oaa_sync"),Z=_(h({id:"ecv-1-5-flat-wrong-audience",workload:{jwksUri:`${K}/workload/certs`,audience:"https://other-app.example/api"},authz:{scopesAsEntitlements:!0}}),{validators:R()});try{await Z.ready($);let G=await _0(s0(Y),Z,"run_oaa_sync",void 0,"workload");z(!G.allow,"a token minted for another audience must not authenticate"),z(G.reason==="unauthenticated","flat audience mismatch -> unauthenticated")}finally{Z.close?.()}}},{name:"subject-disabled marker vetoes the scope overlay; re-enable restores access",fn:async()=>{let K=Q().baseUrl,Y=await S(K,"run_oaa_sync"),Z=new U,G=_(h({id:"ecv-subject-disabled-overlay",workload:{jwksUri:`${K}/workload/certs`,audience:`${K}/workload`},authz:{scopesAsEntitlements:!0}}),{validators:R(),stores:{accessIndex:Z}});try{await G.ready($);let W=s0(Y),H=await _0(W,G,"run_oaa_sync",void 0,"workload");z(H.allow,"the scope overlay allows before the subject is disabled"),await Z.setSubjectDisabled("local-workload-client","workload",!0);let J=await _0(W,G,"run_oaa_sync",void 0,"workload");z(!J.allow,"a disabled subject is denied even when a scope overlay matches (deny wins)"),await Z.setSubjectDisabled("local-workload-client","workload",!1);let j=await _0(W,G,"run_oaa_sync",void 0,"workload");z(j.allow,"re-enabling the subject restores overlay-based access")}finally{G.close?.()}}},{name:"SCIM active flip sets/clears the AccessIndex subject-disabled marker (deactivate, re-activate, delete)",fn:async()=>{let K=Q().baseUrl,Y=await S(K,`${x$.id} ${H1.id}`),Z=new U,G=_(e0(K,"ecv-scim-subject-disabled"),{validators:R(),stores:{accessIndex:Z}});try{await G.ready($);let W={Authorization:`Bearer ${Y}`,"Content-Type":"application/scim+json"},H=await G.handler(new Request("https://ecv.test/api/ionite/iam/Users",{method:"POST",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],userName:"disable-flow@example.test",active:!0})}));z(H.status===201,`SCIM user create should succeed, got ${H.status}`);let J=await H.json();z(!await Z.isSubjectDisabled(J.id,"user"),"an active user starts enabled");let j=await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"PATCH",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"active",value:!1}]})}));z(j.status===200,`SCIM deactivate should succeed, got ${j.status}`),z(await Z.isSubjectDisabled(J.id,"user"),"PATCH active:false sets the disabled marker");let D=await Z.hasPermission(J.id,"read",void 0,"user");z(!D.allow,"a disabled subject is denied by the index"),z(D.metadata?.reason==="subject_disabled","the index denial carries reason subject_disabled");let P=await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"PUT",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],id:J.id,userName:"disable-flow@example.test",active:!0})}));z(P.status===200,`SCIM reactivate should succeed, got ${P.status}`),z(!await Z.isSubjectDisabled(J.id,"user"),"PUT active:true clears the disabled marker"),await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"PATCH",headers:W,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"active",value:!1}]})})),z(await Z.isSubjectDisabled(J.id,"user"),"the marker is set again before delete");let B=await G.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${J.id}`,{method:"DELETE",headers:W}));z(B.status===204,`SCIM user delete should succeed, got ${B.status}`),z(!await Z.isSubjectDisabled(J.id,"user"),"user delete clears the disabled marker")}finally{G.close?.()}}}]}async function j8(X={}){let $=performance.now(),Q=[...J5().tests];if(X.ecv)Q.push(...U$(X)),Q.push(...j5(X));let K=[];for(let W of Q)K.push(await N(W.name,async()=>{await W.fn();return}));let Y=performance.now()-$,Z=K.filter((W)=>!W.passed).length,G=K.filter((W)=>W.details?.skipped).length;return{suite:"Authz",passed:Z===0,tests:K,duration:Y,summary:{total:K.length,passed:K.length-Z-G,failed:Z,skipped:G}}}import{ION_CAPABILITY_CATALOG as W7,ION_CAPABILITY_CATALOG_VERSION as H7}from"@ionite/server";import{silentLogger as w8}from"@ionite/core";import{ciam as D8}from"@ionite/core/server";import{InMemoryMagicLinkStore as B8,InMemorySessionStore as P8}from"@ionite/server";import{createValidators as q8}from"@ionite/valibot";var b$={customerPath:"/api/ionite/auth/customer",magicLinkPath:"/api/ionite/magic-link",magicLinkLoginPath:"/api/ionite/magic-link/login",logoutPath:"/api/ionite/auth/customer/logout",backChannelLogoutPath:"/api/ionite/auth/customer/logout/backchannel",customerProvidersPath:"/api/ionite/auth/customer/providers",customerLoginPath:"/api/ionite/auth/customer/login"},F8=["clientId","clientSecret","authority","privateKey","keyId","teamId"];function L8(X){return X.endsWith(".access")||X.endsWith(".id")||X.endsWith(".control")}function V8(X){for(let $ of X.keys()){if($.endsWith(".session"))return $.slice(0,-8);let Q=$.indexOf(".cstate");if(Q>0)return $.slice(0,Q)}return}async function y$(X,$){return N("CIAM Customer Endpoint (Unauthenticated)",async()=>{let Q=X.customerPath??"/api/ionite/auth/customer",K=await $(Q),Y=K.status!==401?await K.clone().text():"";return z(K.status===401,`Expected 401 Unauthorized, got ${K.status} (content-type=${K.headers.get("content-type")}, body=${Y.slice(0,200)})`),{details:{status:K.status}}})}async function E$(X,$){return N("CIAM Magic Link Requires Workload Auth",async()=>{let Q=X.magicLinkPath??"/api/ionite/magic-link",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userName:"jdoe",name:"Jane Doe",email:"jane@example.com"})});return z(K.status===401||K.status===404,`Expected 401 Unauthorized (or 404 if disabled), got ${K.status}`),{details:{status:K.status,endpointExists:K.status!==404}}})}async function h$(X,$){z($.endsWith("/logout"),`CIAM logoutPath must end with /logout, got: ${$}`);let Q={headers:{Accept:"application/json"}},K=await X($,Q);if(K.status!==200&&K.status!==302)K=await X($,Q);if(K.status!==200&&K.status!==302){let Y=await K.text().catch(()=>"");throw Error(`Expected 302 or 200, got ${K.status} for ${$} (content-type=${K.headers.get("content-type")??"none"}): ${Y.slice(0,500)}`)}return K}async function C$(X,$){return N("CIAM Customer Logout Endpoint",async()=>{let Q=X.logoutPath??"/api/ionite/auth/customer/logout",K=await h$($,Q),Y=K.headers.getSetCookie?.()??[],Z=Y.some((H)=>H.includes("Max-Age=0")||H.includes("expires=Thu, 01 Jan 1970")),G=Y.some((H)=>H.startsWith("ion.sso")&&(H.includes("Max-Age=0")||H.includes("expires=")));z(!G,"CIAM logout must not clear SSO (ion.sso.*) cookies");let W=[...y0(K.headers).entries()].filter(([,H])=>H===""||H.includes("Max-Age=0")).map(([H])=>H);return z(Z||W.length>0||Y.length===0,"CIAM logout should clear customer session cookies"),{details:{status:K.status,clearedCookies:W}}})}async function A$(X,$){return N("CIAM Back-Channel Logout Removed",async()=>{let Q=X.backChannelLogoutPath??"/api/ionite/auth/customer/logout/backchannel",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:""});return z(K.status===404,`Expected 404 (route removed), got ${K.status}`),{details:{status:K.status,endpointRemoved:!0}}})}async function S$(X,$){return N("CIAM Provider Discovery (Sanitized + Ordered)",async()=>{let Q=X.customerProvidersPath??"/api/ionite/auth/customer/providers",K=await $(Q);if(K.status===404)return{details:{status:404,configured:!1,skipped:!0}};z(K.status===200,`Expected 200, got ${K.status}`);let Y=await K.json();z(Array.isArray(Y.providers),"Discovery response must contain a `providers` array");let Z=Y.providers,G=[];for(let H of Z){z(typeof H.name==="string","Each provider must have a string `name`"),z(typeof H.loginUrl==="string","Each provider must include a derived `loginUrl`"),z(typeof H.displayName==="string","Each provider must include a `displayName`");for(let J of F8)z(!(J in H),`Discovery must not expose secret field "${J}"`);G.push(H.name)}let W=await $(Q);if(W.status===200){let J=((await W.json()).providers??[]).map((j)=>j.name);z(JSON.stringify(G)===JSON.stringify(J),"Provider discovery order must be stable (recommended order preserved)")}return{details:{status:200,configured:!0,count:Z.length,order:G}}})}async function k$(X,$){return N("CIAM Third-Party Login Redirect",async()=>{let Q=X.customerLoginPath??"/api/ionite/auth/customer/login",K=await $(`${Q}?provider=__ecv_unknown__`,{redirect:"manual"});z(K.status===404||K.status===400||K.status===500,`Unknown provider should be rejected (4xx/5xx), got ${K.status}`);let Y=X.customerProvidersPath??"/api/ionite/auth/customer/providers",Z=await $(Y);if(Z.status!==200)return{details:{unknownStatus:K.status,configured:!1,skipped:!0}};let W=((await Z.json()).providers??[]).find((P)=>P.type&&P.type!=="magic-link");if(!W?.name)return{details:{unknownStatus:K.status,configured:!1,skipped:!0}};let H=await $(`${Q}?provider=${encodeURIComponent(W.name)}`,{redirect:"manual"});z(H.status===302,`Expected 302 redirect to IdP, got ${H.status}`);let J=H.headers.get("location");z(Boolean(J),"Login redirect must include a Location header"),z((J??"").includes("state=")&&(J??"").includes("client_id="),"Authorization redirect must include OAuth2 state and client_id parameters");let D=(H.headers.getSetCookie?.()??[]).find((P)=>P.includes(".cstate_"));return z(Boolean(D),"Login must set a CIAM state cookie (ion.ciam.cstate_<state>)"),z(/SameSite=Lax/i.test(D),`CIAM state cookie must be SameSite=Lax for the cross-site provider callback, got: ${D}`),{details:{status:302,provider:W.name,unknownStatus:K.status,cstateCookie:D}}})}async function I$(X,$){return N("CIAM Logout Opaque-Only (no token cookies)",async()=>{let Q=X.logoutPath??"/api/ionite/auth/customer/logout",Y=(await h$($,Q)).headers.getSetCookie?.()??[],Z=Y.find((G)=>L8(G.split("=")[0]?.trim()??""));return z(!Z,`CIAM logout must not touch token cookies (opaque-only), found: ${Z}`),{details:{setCookieCount:Y.length}}})}async function m$(X,$){return N("CIAM Legacy Token Cookies Rejected",async()=>{let Q=X.logoutPath??"/api/ionite/auth/customer/logout",K=X.customerPath??"/api/ionite/auth/customer",Y=await $(Q,{headers:{Accept:"application/json"}}),Z=V8(y0(Y.headers));if(!Z)return{details:{skipped:!0,reason:"Could not derive CIAM cookie prefix from logout response"}};let G=new Map([[`${Z}.access`,"legacy-access"],[`${Z}.id`,"legacy-id"],[`${Z}.control`,btoa(JSON.stringify({token_type:"Bearer",expires_in:3600,expires:new Date(Date.now()+3600000).toISOString()}))]]),W=await $(K,{headers:{Cookie:f0(G)}});return z(W.status===401,`Legacy CIAM token cookies must not authenticate (expected 401), got ${W.status}`),{details:{status:W.status,prefix:Z}}})}async function N8(X){let $=performance.now(),Q={...b$,...X},K=r(Q),Y=[];Y.push(await y$(Q,K)),Y.push(await E$(Q,K)),Y.push(await C$(Q,K)),Y.push(await I$(Q,K)),Y.push(await m$(Q,K)),Y.push(await A$(Q,K)),Y.push(await S$(Q,K)),Y.push(await k$(Q,K));let Z=performance.now()-$,G=Y.filter((J)=>J.passed).length,W=Y.filter((J)=>!J.passed).length,H=Y.filter((J)=>J.details?.skipped).length;return{suite:"CIAM",passed:W===0,tests:Y,duration:Z,summary:{total:Y.length,passed:G-H,failed:W,skipped:H}}}async function O8(){return N("CIAM magic-link session stores no token material (§1.10)",async()=>{let X=new B8,$=new P8,Q=q8(),K=D8(Q.ciam,w8,void 0,{magicLinkUrl:"/magic-link",magicLinkLoginUrl:"/magic-link/login",sessionStore:$,magicLinkStore:X,cookiesSecure:!1});z(!!K,"ciam() must return a handle when configured");let Y={id:"cust-1",userName:"cust1",email:"c1@example.test",name:"Customer One"};await X.create("tok-1",Y,new Date(Date.now()+60000));let Z=await K?.handler(new Request("https://app.ecv.local/magic-link/login?token=tok-1"));z(!!Z,"magic-link login must produce a response");let W=(Z?.headers.getSetCookie?.()??[]).find((j)=>(j.split("=")[0]?.trim()??"").endsWith(".session"));z(!!W,"magic-link login must set an opaque session cookie");let H=W.split("=")[1]?.split(";")[0]??"";z(H.length>0,"session cookie must carry an opaque sid");let J=await $.get(H);return z(J!=null,"the magic-link session must be persisted"),z(!("tokens"in J),"a CIAM session must NOT store any signed token material (§1.10)"),z(!!J?.claims,"the opaque session still carries identity claims"),{details:{sid:H,hasTokens:J!=null&&"tokens"in J}}})}function w5(X){let $={...b$,...X},Q=r($),K={createCustomerTests:()=>[{name:"customer endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await y$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"legacy CIAM token cookies do not authenticate (opaque-only)",fn:async()=>{let Z=await m$($,Q);if(!Z.passed)throw Error(Z.error)}}],createMagicLinkTests:()=>[{name:"magic-link generation requires workload auth",fn:async()=>{let Z=await E$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"magic-link session stores no token material (§1.10)",fn:async()=>{let Z=await O8();if(!Z.passed)throw Error(Z.error)}}],createOidcLoginTests:()=>[{name:"provider discovery returns sanitized list in stable order",fn:async()=>{let Z=await S$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"third-party login redirects to IdP with state",fn:async()=>{let Z=await k$($,Q);if(!Z.passed)throw Error(Z.error)}}],createLogoutTests:()=>[{name:"customer logout endpoint clears CIAM cookies (not SSO)",fn:async()=>{let Z=await C$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"customer logout is opaque-only (no token cookies)",fn:async()=>{let Z=await I$($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"customer back-channel logout endpoint is removed (404)",fn:async()=>{let Z=await A$($,Q);if(!Z.passed)throw Error(Z.error)}}]};return{tests:[...K.createCustomerTests(),...K.createMagicLinkTests(),...K.createOidcLoginTests(),...K.createLogoutTests()],ext:K}}import{generateLfvEd25519KeyPair as R8,stringifyLfvJwks as v8}from"@ionite/core/server";import{ionite as s,lfvVault as U8,secretConfigSource as f$,vaultConfig as T1}from"@ionite/server";import{createValidators as e}from"@ionite/valibot";import{generateLfvEd25519KeyPair as g,signLfvPayload as V2,stringifyLfvJwks as d}from"@ionite/core/server";import{ionite as M8,lfvVault as n}from"@ionite/server";import{createValidators as T8}from"@ionite/valibot";var V={lfvServerUrl:"http://localhost:3592/api/lfv",clientId:"ecv-lfv-client",path:"/ecv/lfv/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"};async function u(X,$,Q,K,Y){let Z=String($.request_id),G="issued_at"in $?$:{...$,issued_at:Date.now()},W=JSON.stringify(G),H=Y??await V2({privateKeyJwk:Q,requestId:Z,rawBody:W});return new Request(`http://localhost${X}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":Z,"X-LFV-Key-Id":K,"X-LFV-Signature":H},body:W})}async function j1(X,$,Q){let K=String(X.request_id),Y=JSON.stringify(X),Z=await V2({privateKeyJwk:$,requestId:K,rawBody:Y});return new Response(Y,{status:200,headers:{"Content-Type":"application/json","X-LFV-Request-Id":K,"X-LFV-Key-Id":Q,"X-LFV-Signature":Z}})}function x8(X){return{async load(){return X},async subscribe(){return},secret:{}}}function D5(){return{tests:[{name:"correctly signed OTP delivery callback is accepted",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-delivery-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),G={type:"otp",request_id:crypto.randomUUID(),path:V.path,action:"read_secret",otp:"signed-otp"},W=await Y.handleDelivery(await u(V.deliveryEndpoint,G,$,"ecv-delivery-kid"));w(W.status,503,"a verified OTP with no owner must fail loudly as unroutable"),w((await W.json()).error,"callback_unroutable","unroutable deliveries use the callback_unroutable code"),Y.close?.()}},{name:"unroutable OTP delivery undoes its replay claim so the same signed callback can be retried",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-unroutable-retry-kid"),K=n({...V,verifyJwks:d(Q)}),Y={type:"otp",request_id:crypto.randomUUID(),path:V.path,action:"read_secret",otp:"unroutable-retry",issued_at:Date.now()},Z=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-unroutable-retry-kid"));w(Z.status,503,"the first unroutable delivery is a retryable 503");let G=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-unroutable-retry-kid"));w(G.status,503,"the same signed callback must be retried, not rejected as a replay"),w((await G.json()).error,"callback_unroutable","the retry stays unroutable until a waiter or transport can own it"),K.close?.()}},{name:"read_secret returns confidential material in the TLS action response",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-update-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),Z=globalThis.fetch;globalThis.fetch=async(G,W)=>{let H=typeof G==="string"?G:G instanceof URL?G.toString():G.url,J=JSON.parse(String(W?.body));if(H.endsWith("/otp")){let j=await u(V.deliveryEndpoint,{type:"otp",request_id:J.request_id,path:J.path,action:J.action,otp:`otp-${J.request_id}`},$,"ecv-update-kid");return w((await Y.handleDelivery(j)).status,200,"OTP callback is accepted"),Response.json({status:"pending"},{status:202})}if(H.endsWith("/secrets/request"))return j1({request_id:J.request_id,status:"completed",path:J.path,data:{data:{marker:"pull-response"},metadata:{path:J.path,version:1}}},$,"ecv-update-kid");throw Error(`Unexpected LFV request: ${H}`)};try{let G=await Y.getSecret(V.path);w(G.marker,"pull-response","the action response supplies the secret")}finally{globalThis.fetch=Z,Y.close?.()}}},{name:"unsigned read_secret action response is rejected before the value is published",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-unsigned-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:200}),Y=globalThis.fetch;globalThis.fetch=async(Z,G)=>{let W=typeof Z==="string"?Z:Z instanceof URL?Z.toString():Z.url,H=JSON.parse(String(G?.body));if(W.endsWith("/otp"))return await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:H.request_id,path:H.path,action:H.action,otp:`otp-${H.request_id}`},$,"ecv-pull-unsigned-kid")),Response.json({status:"pending"},{status:202});return Response.json({request_id:H.request_id,path:H.path,data:{data:{marker:"must-not-publish"},metadata:{path:H.path,version:1}}})};try{let Z="";try{await K.getSecret(V.path)}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("missing signature headers"),"unsigned pull response must fail closed")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"tampered signed read_secret action response is rejected",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-tampered-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:200}),Y=globalThis.fetch;globalThis.fetch=async(Z,G)=>{let W=typeof Z==="string"?Z:Z instanceof URL?Z.toString():Z.url,H=JSON.parse(String(G?.body));if(W.endsWith("/otp"))return await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:H.request_id,path:H.path,action:H.action,otp:`otp-${H.request_id}`},$,"ecv-pull-tampered-kid")),Response.json({status:"pending"},{status:202});let J=await j1({request_id:H.request_id,path:H.path,data:{data:{marker:"original"},metadata:{path:H.path,version:1}}},$,"ecv-pull-tampered-kid");return new Response(JSON.stringify({request_id:H.request_id,path:H.path,data:{data:{marker:"tampered"},metadata:{path:H.path,version:1}}}),{status:200,headers:J.headers})};try{let Z="";try{await K.getSecret(V.path)}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("signature verification failed"),"tampered pull response must fail closed")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"read_secret action response is bound to the redeem request_id",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-request-id-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:200}),Y=globalThis.fetch;globalThis.fetch=async(Z,G)=>{let W=typeof Z==="string"?Z:Z instanceof URL?Z.toString():Z.url,H=JSON.parse(String(G?.body));if(W.endsWith("/otp"))return await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:H.request_id,path:H.path,action:H.action,otp:`otp-${H.request_id}`},$,"ecv-pull-request-id-kid")),Response.json({status:"pending"},{status:202});return j1({request_id:crypto.randomUUID(),path:H.path,data:{data:{marker:"wrong-cycle"},metadata:{path:H.path,version:1}}},$,"ecv-pull-request-id-kid")};try{let Z="";try{await K.getSecret(V.path)}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("header does not match"),"pull response must match the redeem request_id")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"terminal read_secret denial does not retry the OTP cycle",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-terminal-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:500,retryInterval:10}),Y=globalThis.fetch,Z=0,G=0;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z+=1,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-pull-terminal-kid")),Response.json({status:"pending"},{status:202});return G+=1,Response.json({error:"permission_denied"},{status:403})};try{let W=!1;try{await K.getSecret(V.path)}catch{W=!0}z(W,"terminal denial must reject the read"),w(Z,1,"terminal denial must not mint another OTP"),w(G,1,"terminal denial must not retry the action")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"404 read_secret is terminal per-read and carries code not_found",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-pull-404-kid"),K=n({...V,verifyJwks:d(Q),deliveryTimeout:500,retryInterval:10}),Y=globalThis.fetch,Z=0,G=0;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z+=1,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-pull-404-kid")),Response.json({status:"pending"},{status:202});return G+=1,Response.json({error:"not_found"},{status:404})};try{let W;try{await K.getSecret(V.path)}catch(H){W=H}z(W instanceof Error,"404 must reject the read"),w(W.status,404,"404 must surface as status 404"),w(W.code,"not_found","404 must classify as not_found"),w(Z,1,"404 must not mint another OTP"),w(G,1,"404 must not retry the action")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"forged delivery signature is rejected with 401 invalid_signature",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-forged-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),G={request_id:crypto.randomUUID(),path:V.path,data:{data:{marker:"forged"},metadata:{path:V.path,version:1}}},W=await Y.handleDelivery(await u(V.deliveryEndpoint,G,$,"ecv-forged-kid","AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));w(W.status,401,"forged delivery should return 401");let H=await W.json();w(H.error,"invalid_signature","error code should be invalid_signature")}},{name:"delivery without signature headers is rejected",fn:async()=>{let{publicJwks:$}=await g("ecv-missing-kid"),K=await n({...V,verifyJwks:d($)}).handleDelivery(new Request(`http://localhost${V.deliveryEndpoint}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({request_id:crypto.randomUUID(),path:V.path,data:{data:{},metadata:{path:V.path,version:1}}})}));w(K.status,401,"unsigned delivery should return 401")}},{name:"delivery rejects request_id header/body mismatch",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-mismatch-kid"),K=d(Q),Y=n({...V,verifyJwks:K}),Z=crypto.randomUUID(),G=JSON.stringify({request_id:Z,path:V.path,data:{data:{},metadata:{path:V.path,version:1}}}),W=await V2({privateKeyJwk:$,requestId:Z,rawBody:G}),H=await Y.handleDelivery(new Request(`http://localhost${V.deliveryEndpoint}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":crypto.randomUUID(),"X-LFV-Key-Id":"ecv-mismatch-kid","X-LFV-Signature":W},body:G}));w(H.status,401,"mismatched request_id should return 401")}},{name:"delivery is rejected when verifyJwks is not configured",fn:async()=>{let{privateJwk:$}=await g("ecv-no-jwks-kid"),Q=n({...V}),Y={request_id:crypto.randomUUID(),path:V.path,data:{data:{},metadata:{path:V.path,version:1}}},Z=await Q.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-no-jwks-kid"));w(Z.status,401,"delivery without verifyJwks should return 401")}},{name:"correctly signed events callback is accepted and key_id maps to the listener keyId",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-events-kid"),K=n({...V,verifyJwks:d(Q)}),Y=[],Z=K.subscribeEvents?.((D)=>{Y.push({event:D.event,...D.keyId?{keyId:D.keyId}:{}})}),W={request_id:crypto.randomUUID(),path:V.path,event:"updated"},H=await K.handleEvents(await u(V.eventsEndpoint,W,$,"ecv-events-kid"));w(H.status,200,"signed events should return 200");let J={request_id:crypto.randomUUID(),path:V.path,event:"kms_key_parked",key_id:"k-parked"},j=await K.handleEvents(await u(V.eventsEndpoint,J,$,"ecv-events-kid"));w(j.status,200,"signed issuer events should return 200"),w(JSON.stringify(Y),JSON.stringify([{event:"updated"},{event:"kms_key_parked",keyId:"k-parked"}]),"listeners receive the wire key_id as keyId (metadata-only)"),Z?.(),K.close?.()}},{name:"forged events signature is rejected with 401",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-events-forged-kid"),K=n({...V,verifyJwks:d(Q)}),Z={request_id:crypto.randomUUID(),path:V.path,event:"revoked"},G=await K.handleEvents(await u(V.eventsEndpoint,Z,$,"ecv-events-forged-kid","BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"));w(G.status,401,"forged events should return 401")}},{name:"replayed delivery callback (same signature) is rejected as a replay",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-replay-kid"),K=n({...V,verifyJwks:d(Q)},{put:async()=>{},take:async()=>{return}}),Y={type:"otp",request_id:crypto.randomUUID(),path:V.path,action:"read_secret",otp:"single-use-replay-test",issued_at:Date.now()},Z=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-replay-kid"));w(Z.status,200,"first delivery should be accepted");let G=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-replay-kid"));w(G.status,401,"an exact replay of the same signed callback must be rejected");let W=await G.json();w(W.error,"invalid_signature","replay rejection uses the invalid_signature code")}},{name:"stale delivery callback (issued_at outside freshness window) is rejected",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-stale-kid"),K=n({...V,verifyJwks:d(Q)}),Y={request_id:crypto.randomUUID(),path:V.path,issued_at:Date.now()-600000,data:{data:{marker:"stale"},metadata:{path:V.path,version:1}}},Z=await K.handleDelivery(await u(V.deliveryEndpoint,Y,$,"ecv-stale-kid"));w(Z.status,401,"a stale callback must be rejected")}},{name:"delivery callbacks reject secret values and accept OTP work items only",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-otp-only-kid"),Y=await n({...V,verifyJwks:d(Q)}).handleDelivery(await u(V.deliveryEndpoint,{request_id:crypto.randomUUID(),path:V.path,data:{data:{marker:"must-not-transit"},metadata:{path:V.path,version:1}}},$,"ecv-otp-only-kid"));w(Y.status,400,"a value-bearing callback is rejected"),w((await Y.json()).error,"invalid_request","the callback is rejected as invalid_request")}},{name:"callback fan-out remains scoped by verifyJwks trust identity",fn:async()=>{let $=await g("ecv-trust-a"),Q=await g("ecv-trust-b"),K={...V,path:"/ecv/lfv/trust-shared"},Y={put:async()=>{},take:async()=>{return}},Z=n({...K,verifyJwks:d($.publicJwks)},Y),G=n({...K,verifyJwks:d(Q.publicJwks)},Y);w(Z.callbackKey,G.callbackKey,"both sources share one routing key"),z(Z.trustId!==G.trustId,"sources preserve distinct trust identities");let W=await Z.handleDelivery(await u(K.deliveryEndpoint,{type:"otp",request_id:crypto.randomUUID(),path:K.path,action:"read_secret",otp:"trust-a-only"},$.privateJwk,"ecv-trust-a"));w(W.status,200,"source A accepts its trusted OTP"),Z.close?.(),G.close?.()}},{name:"failed post-event refresh marks the source degraded (isAvailable() false), not a stale serve",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-degraded-kid"),K=d(Q),Y=n({...V,verifyJwks:K,deliveryTimeout:150,retryInterval:40});w(Y.isAvailable?.(),!0,"a newly-created source is available"),await Y.handleEvents(await u(V.eventsEndpoint,{request_id:crypto.randomUUID(),path:V.path,event:"updated"},$,"ecv-degraded-kid"));let Z=Date.now()+3000;while(Y.isAvailable?.()!==!1&&Date.now()<Z)await new Promise((G)=>setTimeout(G,25));w(Y.isAvailable?.(),!1,"a failed post-event refresh must mark the source degraded via isAvailable()"),Y.close?.()}},{name:"cacheMaxAgeSeconds bounds cache hits: a stale entry forces a fresh read",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-maxage-kid"),K=d(Q),Y=n({...V,verifyJwks:K,cacheMaxAgeSeconds:0.05,deliveryTimeout:150,retryInterval:40}),Z=globalThis.fetch,G=0;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return await Y.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-maxage-kid")),Response.json({status:"pending"},{status:202});return G+=1,j1({request_id:j.request_id,path:j.path,data:{data:{marker:G===1?"fresh":"refreshed"},metadata:{path:j.path,version:G}}},$,"ecv-maxage-kid")};try{w((await Y.getSecret(V.path)).marker,"fresh","the initial pull is cached"),await new Promise((W)=>setTimeout(W,120)),w((await Y.getSecret(V.path)).marker,"refreshed","an aged cache entry forces another pull"),w(G,2,"the action endpoint was called twice")}finally{globalThis.fetch=Z,Y.close?.()}}},{name:"raised kms_slots demand bypasses the cache and is carried on OTP issue and redeem",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-slots-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z=0,G=[],W=[];globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H instanceof URL?H.toString():H.url,D=JSON.parse(String(J?.body));if(j.endsWith("/otp"))return G.push(D.kms_slots),await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:D.request_id,path:D.path,action:D.action,otp:`otp-${D.request_id}`},$,"ecv-kms-slots-kid")),Response.json({status:"pending"},{status:202});return W.push(D.kms_slots),Z+=1,j1({request_id:D.request_id,path:D.path,data:{data:{marker:`grant-v${Z}`},metadata:{path:D.path,version:Z}}},$,"ecv-kms-slots-kid")};try{let H=[],J=await K.subscribe(V.path,(q)=>{H.push(q.metadata.version)});w(Z,1,"the initial subscribe performs one read cycle"),w(G[0],void 0,"an undeclared demand omits kms_slots from the OTP request"),w(W[0],void 0,"an undeclared demand omits kms_slots from the redeem");let j,D=await K.subscribe(V.path,(q)=>{j=q.metadata.version},{kmsSlots:3});w(Z,2,"a raised demand bypasses the cache and forces a fresh read"),w(G[1],3,"the raised demand is carried as kms_slots on OTP issuance"),w(W[1],3,"the raised demand is carried as kms_slots on the redeem"),w(j,2,"the raising subscriber receives the freshly materialized grant");let P,B=await K.subscribe(V.path,(q)=>{P=q.metadata.version},{kmsSlots:2});w(Z,2,"a demand at or below the recorded high-water mark is served from cache"),w(P,2,"the cached grant already satisfies the lower demand"),J(),D(),B()}finally{globalThis.fetch=Y,K.close?.()}}},{name:"kms_park OTP issue and redeem carry key_id, status, and reason and never the secret value",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-park-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z,G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z=j.action,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-kms-park-kid")),Response.json({status:"pending"},{status:202});return G={action:j.action,key_id:j.key_id,status:j.status,reason:j.reason,value:j.value},Response.json({request_id:j.request_id,parked:!0,disposition:"cooldown"})};try{let W=await K.parkKmsKey(V.path,{keyId:"k-parked",status:429,reason:"rate limited"});w(Z,"kms_park","OTP issuance must name the kms_park action"),w(G?.key_id,"k-parked","the redeem body carries the resolved key id"),w(G?.status,429,"the redeem body carries the reported status"),w(G?.reason,"rate limited","the redeem body carries the reported reason"),z(G?.value===void 0,"the redeem body must never include a key value"),w(W.parked,!0,"the source returns the vault park result"),w(W.disposition,"cooldown","the source returns the vault disposition")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"kms_add_keys OTP issue and redeem carry issued keys and return assigned ids",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-add-keys-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z,G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z=j.action,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-kms-add-keys-kid")),Response.json({status:"pending"},{status:202});return G=j.keys,Response.json({request_id:j.request_id,keys:[{id:"issued-1"}]})};try{let W=await K.addKmsKeys(V.path,{keys:[{value:"oem-token"}]});w(Z,"kms_add_keys","OTP issuance must name the kms_add_keys action"),w(JSON.stringify(G),JSON.stringify([{value:"oem-token"}]),"the redeem body carries issued keys"),w(W.keys[0]?.id,"issued-1","the source returns assigned key ids")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"kms_revoke_key OTP issue and redeem carry key_id and reason",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await g("ecv-kms-revoke-key-kid"),K=n({...V,verifyJwks:d(Q)}),Y=globalThis.fetch,Z,G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp"))return Z=j.action,await K.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:j.request_id,path:j.path,action:j.action,otp:`otp-${j.request_id}`},$,"ecv-kms-revoke-key-kid")),Response.json({status:"pending"},{status:202});return G={key_id:j.key_id,reason:j.reason},Response.json({request_id:j.request_id,revoked:!0})};try{let W=await K.revokeKmsKey(V.path,{keyId:"k-revoked",reason:"compromise"});w(Z,"kms_revoke_key","OTP issuance must name the kms_revoke_key action"),w(G?.key_id,"k-revoked","the redeem body carries the key id"),w(G?.reason,"compromise","the redeem body carries the reason"),w(W.revoked,!0,"the source returns the vault revoke result")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"two LFV sources on one serverUrl|path with different verifyJwks are rejected at apply",fn:async()=>{let $=d((await g("ecv-apply-a")).publicJwks),Q=d((await g("ecv-apply-b")).publicJwks),K="/ecv/lfv/apply-shared",Y={id:"ecv-lfv-dual-trust",secrets:{SRC_A:{type:"vault",lfv:{lfvServerUrl:V.lfvServerUrl,clientId:"ecv-apply-client",path:"/ecv/lfv/apply-shared",deliveryEndpoint:"/api/a/lfv/delivery",eventsEndpoint:"/api/a/lfv/events",verifyJwks:$}},SRC_B:{type:"vault",lfv:{lfvServerUrl:V.lfvServerUrl,clientId:"ecv-apply-client",path:"/ecv/lfv/apply-shared",deliveryEndpoint:"/api/b/lfv/delivery",eventsEndpoint:"/api/b/lfv/events",verifyJwks:Q}}}},Z=M8(x8(Y),{validators:T8(),lfv:{singleProcess:!0}}),G=!1,W="";try{await Z.ready(2000)}catch(H){G=!0,W=H instanceof Error?H.message:String(H)}Z.close?.(),z(G,"a dual-trust LFV config must reject ion.ready() at apply time"),z(W.toLowerCase().includes("trust key"),`apply rejection should explain the single-trust-key-per-path invariant (got: ${W})`)}}]}}async function _8(){let X=performance.now(),{tests:$}=D5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"LFV callbacks",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}function I1(X,$={}){return Object.assign(Error(X),$)}function M1(X){if(X.length===0)throw Error("sequenceConfigSource requires at least one step");let $=0;return{attempts:()=>$,async load(){$+=1;let Q=X[Math.min($-1,X.length-1)];if(Q===void 0)throw Error("sequenceConfigSource step unexpectedly missing");if(Q instanceof Error)throw Q;if(typeof Q==="function")return await Q();return Q},async subscribe(){return},secret:{}}}async function N0(X){await new Promise(($)=>setTimeout($,X))}function E0(X){let $=X,Q;return{async load(){return $},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){$=K,Q?.(K)},secret:{}}}function p$(X,$){let Q;return{async load(){return await N0($),X},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){Q?.(K)},secret:{}}}var w0={jwksUri:"http://localhost/workload/certs",issuer:"http://localhost/workload",audience:"http://localhost/workload"};function N2(){return[{name:"module readiness re-arms when a module is de-configured and re-configured (§5.1)",fn:async()=>{let X=E0({id:"rearm",workload:w0}),$=s(X,{validators:e()});await $.ready(500),z($.workload?.isReady?.()===!0,"Expected workload ready after initial config"),X.push({id:"rearm"}),await N0(20),z($.workload?.isReady?.()===!1,"Expected workload.isReady() false after de-configuration");let Q=!1;try{await $.workload?.ready?.(50)}catch{Q=!0}z(Q,"Expected re-armed workload.ready(50) to reject while de-configured"),X.push({id:"rearm",workload:w0}),await N0(20),z($.workload?.isReady?.()===!0,"Expected workload.isReady() true after re-configuration"),await $.workload?.ready?.(200),$.close?.()}},{name:"async beforeChange override is awaited and applied (§5.2)",fn:async()=>{let X=E0({id:"bc-1"}),$=s(X,{validators:e(),beforeChange:async(Q)=>{return await N0(5),{config:{...Q,id:"bc-1-overridden"}}}});await $.ready(500),w($.config?.id,"bc-1-overridden","Expected async beforeChange override to be applied"),$.close?.()}},{name:"awaiting ion.ready() inside beforeChange on initial load fails fast (§5.2)",fn:async()=>{let X=E0({id:"deadlock-1"}),$=!1,Q=s(X,{validators:e(),beforeChange:async()=>{try{await Q.ready(100)}catch{$=!0}return}});await Q.ready(500),z($,"Expected ion.ready() inside beforeChange to reject (deadlock guard)"),z(Q.isReady(),"Expected instance to still become ready after beforeChange returns"),Q.close?.()}},{name:"no-argument ion.ready() is bounded by readyTimeoutMs; 0 opts into an indefinite wait",fn:async()=>{let X=p$({id:"ready-default-bound"},300),$=s(X,{validators:e(),readyTimeoutMs:50}),Q=!1;try{await $.ready()}catch{Q=!0}z(Q,"Expected the no-argument ion.ready() to reject after the readyTimeoutMs bound"),await $.ready(0),z($.isReady(),"Expected ion.ready(0) to wait indefinitely and resolve once config applied"),$.close?.()}},{name:"throwing afterChange on initial load rejects ready() (§5.4)",fn:async()=>{let X=E0({id:"ac-1",workload:w0}),$=s(X,{validators:e(),afterChange:()=>{throw Error("afterChange boom (initial)")}}),Q=!1;try{await $.ready(500)}catch{Q=!0}z(Q,"Expected a throwing initial afterChange to reject ready()"),$.close?.()}},{name:"setStores() re-applies config and runs afterChange (§16.2)",fn:async()=>{let X=0,$=!1,Q=E0({id:"setstores-1",workload:w0}),K=s(Q,{validators:e(),afterChange:(Z,G,W)=>{X+=1,$=typeof W.setStores==="function"}});await K.ready(500);let Y=X;z(Y>=1,"Expected afterChange to run on the initial apply"),await K.setStores?.({}),z(X>Y,"Expected afterChange to run again after setStores() re-applies config"),z($,"Expected afterChange to receive a modifiable framework config (with setStores) on the store swap"),K.close?.()}},{name:"throwing afterChange on a live push keeps last-known-good applied (§5.4)",fn:async()=>{let X=0,$=E0({id:"aclkg"}),Q=s($,{validators:e(),afterChange:()=>{if(X+=1,X>=2)throw Error("afterChange boom (live)")}});await Q.ready(500),z(Q.isReady(),"Expected ready after first apply"),$.push({id:"aclkg",workload:w0}),await N0(20),z(Q.isReady(),"Expected instance to stay ready (LKG) after a throwing live afterChange"),z(Q.config?.workload!==void 0,"Expected the pushed config to still apply despite afterChange throwing"),Q.close?.()}},{name:"a live update that changes the immutable RemoteConfig.id is rejected and keeps last-known-good (§2.4)",fn:async()=>{let X=E0({id:"immutable-id",workload:w0}),$=s(X,{validators:e()});await $.ready(500),w($.config?.id,"immutable-id","Expected the initial id to apply"),X.push({id:"different-id",workload:w0}),await N0(20),z($.isReady(),"Expected the instance to stay ready after a rejected id change"),w($.config?.id,"immutable-id","Expected the immutable id change to be rejected (LKG retained)"),z($.configStatus?.lastError!==void 0,"Expected configStatus.lastError to record the rejected immutable-id change"),$.close?.()}},{name:"setStores() and a concurrent live push are serialized; the push is reflected (§5.1)",fn:async()=>{let X=E0({id:"s51",workload:w0}),$=s(X,{validators:e(),beforeChange:async(K)=>{return await N0(50),{config:K}}});await $.ready(1000),z($.config?.capabilitiez!==!0,"Expected capabilitiez unset before the push");let Q=$.setStores?.({});X.push({id:"s51",workload:w0,capabilitiez:!0}),await Q,await N0(150),w($.config?.capabilitiez,!0,"Expected the concurrent live push to be reflected after setStores (serialized, not reverted)"),$.close?.()}},{name:"a wait-mode push that lands during a slow initial load wins over the stale load (§5.2)",fn:async()=>{let X=p$({id:"race",workload:w0},80),$=s(X,{validators:e()});X.push({id:"race",workload:w0,capabilitiez:!0}),await $.ready(500),w($.config?.capabilitiez,!0,"Expected the wait-mode push (v2) to be the applied config"),await N0(120),w($.config?.capabilitiez,!0,"Expected the stale initial load to be skipped (push retained)"),$.close?.()}},{name:"a secrets source config unchanged across an sso-only re-apply keeps a stable, ready source (§7.9)",fn:async()=>{let X=E0({id:"memo",workload:w0,secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),$=s(X,{validators:e()});await $.ready(500),z($.secrets?.isReady?.()===!0,"Expected the secrets module ready after initial apply");let Q=$.secrets?.getSecretsSource("primary");X.push({id:"memo",workload:w0,sso:{clientId:"c",clientSecret:"s",authority:"http://localhost/idp"},secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),await N0(30),z($.secrets?.isReady?.()===!0,"Expected the secrets module to stay ready after an sso-only change");let K=$.secrets?.getSecretsSource("primary");z(Q===K,"Expected the unchanged secrets source handle to be reused (memoized) across apply"),$.close?.()}}]}function B5(X){return{type:"vault",getFullSecret:async()=>({data:X,metadata:{version:1}})}}function g$(X){return{type:"vault",getSecret:async()=>X}}async function m1(X,$){try{await X()}catch(Q){if($&&!$(Q))throw Error(`Threw an unexpected error: ${Q instanceof Error?Q.message:String(Q)}`);return Q}throw Error("Expected the operation to throw, but it resolved")}function O2(){return[{name:"RemoteConfig credential wins over a framework-set secrets credential (§2.1)",fn:async()=>{let X=e(),$={},Q={...X,secrets:{...X.secrets,validateSourceConfig:(Z,G)=>{$[Z]=G}}},K=E0({id:"merge-1",secrets:{primary:{type:"vault",url:"http://localhost/vault",token:"REMOTE-TOKEN"}}}),Y=s(K,{validators:Q,secrets:{primary:{token:"CODE-TOKEN"}}});await Y.ready(500),w($.primary?.token,"REMOTE-TOKEN","Expected the merged secrets config to use the RemoteConfig token, not the framework token"),Y.close?.()}},{name:"unimplemented secrets source types (aws/gcp/dev) fail at config apply, not first access (§4.3)",fn:async()=>{for(let X of["aws","gcp","dev"]){let $=E0({id:`unimpl-${X}`,secrets:{x:{type:X}}}),Q=s($,{validators:e()}),K=!1;try{await Q.ready(300)}catch{K=!0}z(K,`Expected secrets source type "${X}" to fail readiness at config apply`),Q.close?.()}}},{name:"vault config-source rejects an empty payload as recoverable and a wrong-shape payload as invalid (§2.2)",fn:async()=>{let X=T1({vault:B5({}),path:"ion/config"});await m1(()=>X.load(),(Y)=>Y instanceof Error&&(Y.name==="VaultConfigWaitingForSecretError"||Y.code==="not_found"||/empty|missing/i.test(Y.message)));let $=T1({vault:B5({foo:1}),path:"ion/config"});await m1(()=>$.load(),(Y)=>Y instanceof Error&&/invalid payload|not a remoteconfig/i.test(Y.message));let K=await T1({vault:B5({data:{id:"cfg-ok"}}),path:"ion/config"}).load();w(K.id,"cfg-ok","Expected a valid wrapped RemoteConfig payload to be unwrapped")}},{name:"vault config-source enforces the ten-minute HTTP polling floor",fn:async()=>{let X=B5({data:{id:"polling-ttl"}});await m1(async()=>T1({vault:X,path:"ion/config",ttl:599999}),(K)=>K instanceof Error&&/10 minutes|600000/.test(K.message));let Q=await T1({vault:X,path:"ion/config",ttl:600000}).load();w(Q.id,"polling-ttl","the minimum safe HTTP polling TTL is accepted")}},{name:"secret config-source rejects an invalid ConfigLocator payload (§2.2)",fn:async()=>{let X=f$(g$({type:"not-a-real-type"}),"boot/locator");await m1(()=>X.load(),(Q)=>Q instanceof Error&&/invalid payload|unknown type/i.test(Q.message));let $=f$(g$("vault"),"boot/locator");await m1(()=>$.load(),(Q)=>Q instanceof Error&&/invalid payload|expected an object/i.test(Q.message))}},{name:"a configured beforeChange re-applies even when the effective config is unchanged (§2.3)",fn:async()=>{let X=0,$=E0({id:"normalize-me",workload:w0}),Q=s($,{validators:e(),beforeChange:(Y)=>({config:{...Y,id:"normalized"}}),afterChange:()=>{X+=1}});await Q.ready(500);let K=X;z(K>=1,"Expected at least one apply on the initial load"),$.push({id:"normalize-me-2",workload:w0}),await N0(30),z(X>K,"Expected beforeChange-present pipeline to re-apply on a normalized push"),Q.close?.()}},{name:"secrets module stays not-ready while a declared source lacks a ConfigSource type (§4.5)",fn:async()=>{let X=E0({id:"sec-ready",workload:w0}),$=s(X,{validators:e(),secrets:{needsRemote:{}}});await $.ready(500),z($.isReady(),"Expected the instance to be ready (config applied) on initial load"),z($.secrets?.isReady?.()===!1,"Expected the secrets module to be not-ready while a declared source has no ConfigSource type"),X.push({id:"sec-ready",workload:w0,secrets:{needsRemote:{type:"vault",url:"http://localhost/vault"}}}),await N0(30),z($.secrets?.isReady?.()===!0,"Expected the secrets module to become ready once the ConfigSource supplies the source type"),$.close?.()}}]}function M2(){return[{name:"retries recoverable missing config and then becomes ready",fn:async()=>{let X=M1([I1("Waiting for vault config secret at ion/test",{code:"not_found",status:404,sourceType:"vault"}),{id:"retry-ok"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await $.ready(250),w(X.attempts(),2,"Expected one retry after recoverable missing config"),$.close?.()}},{name:"retries an LFV 404 not_found then becomes ready (§2.1 transport parity)",fn:async()=>{let{privateJwk:X,publicJwks:$}=await R8("ecv-lfv-404-retry-kid"),Q=U8({...V,verifyJwks:v8($),deliveryTimeout:500,retryInterval:10}),K=T1({path:V.path,vault:Q}),Y=0,Z={attempts:()=>Y,load:async()=>{return Y+=1,K.load()},subscribe:async()=>{return},secret:K.secret},G=globalThis.fetch;globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H instanceof URL?H.toString():H.url,D=JSON.parse(String(J?.body));if(j.endsWith("/otp")){if(Y<=1)return Response.json({error:"not_found"},{status:404});return await Q.handleDelivery(await u(V.deliveryEndpoint,{type:"otp",request_id:D.request_id,path:D.path,action:D.action,otp:`otp-${D.request_id}`},X,"ecv-lfv-404-retry-kid")),Response.json({status:"pending"},{status:202})}if(j.endsWith("/secrets/request"))return j1({request_id:D.request_id,status:"completed",path:D.path,data:{data:{id:"lfv-404-retry-ok"},metadata:{path:D.path,version:1}}},X,"ecv-lfv-404-retry-kid");throw Error(`Unexpected LFV request: ${j}`)};let W=s(Z,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});try{await W.ready(250),z(W.isReady(),"Expected LFV 404 not_found to retry and then become ready"),w(Z.attempts(),2,"Expected one retry after an LFV 404"),w(W.config?.id,"lfv-404-retry-ok","Expected the recovered LFV config to apply")}finally{globalThis.fetch=G,W.close?.(),Q.close?.()}}},{name:"fails fast for unrecoverable auth errors",fn:async()=>{let X=M1([I1("Vault returned 403 Forbidden (auth error)",{status:403,sourceType:"vault"}),{id:"should-not-load"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}}),Q=!1;try{await $.ready(250)}catch{Q=!0}await N0(10),z(Q,"Expected auth failure to reject readiness"),w(X.attempts(),1,"Expected auth failure not to retry"),$.close?.()}},{name:"retries unclassified (unknown) errors instead of failing fast (§2.1)",fn:async()=>{let X=M1([Error("unexpected config loader bug"),{id:"unknown-retry-ok"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await $.ready(250),z($.isReady(),"Expected an unknown error to retry and then become ready"),w(X.attempts(),2,"Expected one retry after an unknown (retry-by-default) error"),$.close?.()}},{name:"retries transient transport codes (EAI_AGAIN) then becomes ready (§2.1)",fn:async()=>{let X=M1([I1("getaddrinfo EAI_AGAIN vault.internal",{code:"EAI_AGAIN",sourceType:"vault"}),I1("getaddrinfo EAI_AGAIN vault.internal",{code:"EAI_AGAIN",sourceType:"vault"}),{id:"eai-ok"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await $.ready(500),z($.isReady(),"Expected EAI_AGAIN to retry and then become ready"),w(X.attempts(),3,"Expected two retries after EAI_AGAIN before success"),$.close?.()}},{name:"fails fast for a structured 4xx (403) load error (§2.1)",fn:async()=>{let X=M1([I1("Vault returned 403 Forbidden",{status:403,sourceType:"vault"}),{id:"should-not-load"}]),$=s(X,{validators:e(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}}),Q=!1;try{await $.ready(250)}catch{Q=!0}await N0(10),z(Q,"Expected a 403 load to fail fast"),w(X.attempts(),1,"Expected a provably-unrecoverable 403 not to retry"),$.close?.()}},{name:"handler returns 503 immediately while the instance is not ready",fn:async()=>{let X=M1([async()=>{return await N0(100),{id:"slow-config"}}]),$=s(X,{validators:e()});z(!$.isReady(),"Expected instance to start not ready");let Q=await $.handler(new Request("http://localhost/api/ionite/auth/user"));w(Q.status,503,"Expected handler to return 503 immediately when not ready"),await $.ready(250),z($.isReady(),"Expected instance to be ready after config applied"),$.close?.()}}]}import{ION_CAPABILITIEZ_READ as b8,ION_IAM_GROUPS_WRITE as y8,ION_IAM_READ as E8,ION_IAM_USERS_WRITE as h8,ION_TENANT_MANAGE as C8}from"@ionite/server";var T2=`${E8.id} ${h8.id} ${y8.id}`,x2=b8.id,f1=C8.id;async function k0(X,$,Q="local-workload-client",K="local-workload-secret"){let Y=`${X.replace(/\/$/,"")}/workload/token`,Z=await fetch(Y,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:Q,client_secret:K,...$?{scope:$}:{}}).toString()});if(!Z.ok){let W=await Z.text();throw Error(`Failed to get tenant auth token: ${Z.status} ${W}`)}let G=await Z.json();if(!G.access_token)throw Error("Tenant auth token response missing access_token");return G.access_token}function _2(){return[{name:"readyPath defaults to basePath + /readyz (/api/ionite/readyz) (§17.3)",fn:async()=>{w(L1({}),"/api/ionite/readyz","Expected the default readyPath to derive from /api/ionite"),w(L1({basePath:"/api/ionite/"}),"/api/ionite/readyz","Expected a trailing-slash basePath to normalize")}},{name:"readyPath derives from a custom basePath (§17.3)",fn:async()=>{w(L1({basePath:"/custom/mount"}),"/custom/mount/readyz","Expected readyPath to derive from the provided basePath")}},{name:"an explicit readyPath always wins over basePath (§17.3)",fn:async()=>{w(L1({readyPath:"/api/readyz",basePath:"/api/ionite"}),"/api/readyz","Expected an explicit readyPath to override basePath derivation")}}]}import{ION_SCIM_USER_EXTENSION_URN as R0}from"@ionite/core";import{createValidators as A8}from"@ionite/valibot";var R2=A8(),S8="urn:ionite:params:scim:schemas:extension:authz:2.0:Group",P5="urn:ietf:params:scim:schemas:core:2.0:User",p1="urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",k8="urn:ietf:params:scim:api:messages:2.0:ListResponse",u$="ecid:ecv.ionite.test:01J9F3Z8QK7M2N4P6R8T0V2W4X";function m(X,$){return z(typeof X==="object"&&X!==null&&!Array.isArray(X),`${$} must be an object`),X}function v0(X,$){return z(typeof X==="string",`${$} must be a string`),X}function l$(X,$){return z(typeof X==="number",`${$} must be a number`),X}function g1(X,$){return z(Array.isArray(X)&&X.every((Q)=>typeof Q==="string"),`${$} must be a string array`),X}function $1(X,$){return z(Array.isArray(X),`${$} must be an array`),X}function q5(X,$){let Q=X[S8];if(Q===void 0)return;let K=m(Q,`${$} authz extension`);if(K.type!==void 0)z(K.type==="group"||K.type==="role"||K.type==="permission",`${$} authz extension type must be group, role, or permission`);if(K.requestable!==void 0)z(typeof K.requestable==="boolean",`${$} authz extension requestable must be a boolean`);if(K.requestUrl!==void 0)z(typeof K.requestUrl==="string",`${$} authz extension requestUrl must be a string`),new URL(K.requestUrl);if(K.description!==void 0)z(typeof K.description==="string",`${$} authz extension description must be a string`)}function v2(X,$,Q,K){let Y=m($.meta,`Created ${Q} meta`),Z=v0(Y.location,`Created ${Q} meta.location`),G=X.headers.get("Location");z(G===Z,`Location header must equal meta.location for created ${Q}`),z(new URL(Z).pathname.endsWith(`/${Q}/${encodeURIComponent(K)}`),`Created ${Q} location must address the new resource id`)}var d$={scimPath:"/api/iam",testUser:{userName:"ecv-test-user@example.com",displayName:"ECV Test User",emails:[{value:"ecv-test-user@example.com",primary:!0}],name:{givenName:"ECV Test",familyName:"User"},[p1]:{department:"Platform",costCenter:"ENG-001",organization:"ionite"},[R0]:{ecid:u$,accountClass:"daily"}},testGroup:{displayName:"ECV Test Group",externalId:"ecv-test-group-001"}};async function H0(X){let $=typeof X.ion==="function"?X.ion():X.ion;if($!==null&&$!==void 0){if(await $.ready(X.timeout??15000),!$.workload)throw Error("Workload management is required for IAM tests. Ionite instance provided but workload is not configured.");let K=$.workload;return`Bearer ${typeof K.getServerToken==="function"?await K.getServerToken():await K.getToken()}`}return`Bearer ${await k0(V1(X.ecvUrl,"IAM validation ecvUrl when ion is not provided"),T2)}`}async function I8(X){return`Bearer ${await k0(V1(X.ecvUrl,"IAM validation ecvUrl when ion is not provided"))}`}async function c$(X,$){return N("IAM Users Endpoint Schema",async()=>{let Q=await H0(X),K=await $(`${X.scimPath}/Users`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(K.status===200,`Expected 200 OK, got ${K.status}`);let Y=m(await K.json(),"SCIM users response"),Z=g1(Y.schemas,"Response schemas"),G=l$(Y.totalResults,"Response totalResults"),W=$1(Y.Resources,"Response Resources");return z(Z.includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"),"Response missing SCIM ListResponse schema"),{details:{totalResults:G,resourceCount:W.length}}})}async function m8(X,$){return N("IAM Groups Endpoint Schema",async()=>{let Q=await H0(X),K=await $(`${X.scimPath}/Groups`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(K.status===200,`Expected 200 OK, got ${K.status}`);let Y=m(await K.json(),"SCIM groups response"),Z=g1(Y.schemas,"Response schemas"),G=l$(Y.totalResults,"Response totalResults"),W=Y.Resources===void 0?[]:$1(Y.Resources,"Response Resources");for(let H of W)q5(m(H,"SCIM group resource"),"SCIM group resource");return z(Z.includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"),"Response missing SCIM ListResponse schema"),{details:{totalResults:G,resourceCount:W.length}}})}async function n$(X,$){if(!X.testUser)return{...W1("IAM Create User","No test user configured"),userId:void 0};let Q=X.testUser,K=await N("IAM Create User",async()=>{let Y=await H0(X),Z={schemas:[P5,p1,...Q[R0]?[R0]:[]],...Q},G=await $(`${X.scimPath}/Users`,{method:"POST",headers:{Authorization:Y,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify(Z)});z(G.status===201,`Expected 201 Created, got ${G.status}`);let W=m(await G.json(),"Created SCIM user");r0(W,R2.iam.user);let H=g1(W.schemas,"Created user schemas"),J=v0(W.id,"Created user id"),j=v0(W.userName,"Created user userName");if(v2(G,W,"Users",J),z(H.includes(P5),"Created user missing SCIM User schema"),Q[p1])z(H.includes(p1),"Created user missing enterprise extension schema");if(Q[R0]){z(H.includes(R0),"Created user missing ionite User extension schema");let D=m(W[R0],"Created user ionite extension");z(D.ecid===u$,"Created user must round-trip its ECID"),z(D.accountClass==="daily","Created user must round-trip its accountClass")}return{details:{userId:J,userName:j}}});return{...K,userId:K.details?.userId}}async function i$(X,$,Q){return N("IAM Get User",async()=>{let K=await H0(X),Y=await $(`${X.scimPath}/Users/${Q}`,{headers:{Authorization:K,Accept:"application/scim+json"}});z(Y.status===200,`Expected 200 OK, got ${Y.status}`);let Z=m(await Y.json(),"Fetched SCIM user");r0(Z,R2.iam.user);let G=v0(Z.id,"Fetched user id"),W=v0(Z.userName,"Fetched user userName");z(G===Q,`User ID mismatch: expected ${Q}, got ${G}`);let H=X.testUser?.[p1];if(H){let j=m(Z["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"Fetched user enterprise extension");if(H.department!==void 0)z(j.department===H.department,`Expected department ${H.department}, got ${String(j.department)}`);if(H.costCenter!==void 0)z(j.costCenter===H.costCenter,`Expected costCenter ${H.costCenter}, got ${String(j.costCenter)}`)}let J=X.testUser?.[R0];if(J){let j=m(Z[R0],"Fetched user ionite extension");z(j.ecid===J.ecid,"Fetched user must preserve its ECID"),z(j.accountClass===J.accountClass,"Fetched user must preserve its accountClass")}return{details:{userId:G,userName:W}}})}async function o$(X,$,Q){return N("IAM Update User",async()=>{let K=await H0(X),Y={schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:"ECV Test User (Updated)"}]},Z=await $(`${X.scimPath}/Users/${Q}`,{method:"PATCH",headers:{Authorization:K,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify(Y)});z(Z.status===200,`Expected 200 OK, got ${Z.status}`);let G=m(await Z.json(),"Updated SCIM user"),W=v0(G.id,"Updated user id"),H=v0(G.displayName,"Updated user displayName");return z(H==="ECV Test User (Updated)","displayName was not updated"),{details:{userId:W,displayName:H}}})}async function a$(X,$,Q){return N("IAM Delete User",async()=>{let K=await H0(X),Y=await $(`${X.scimPath}/Users/${Q}`,{method:"DELETE",headers:{Authorization:K}});z(Y.status===204,`Expected 204 No Content, got ${Y.status}`);let Z=await $(`${X.scimPath}/Users/${Q}`,{headers:{Authorization:K,Accept:"application/scim+json"}});return z(Z.status===404,`Expected 404 after deletion, got ${Z.status}`),{details:{userId:Q,deleted:!0}}})}async function t$(X,$){if(!X.testGroup)return{...W1("IAM Create Group","No test group configured"),groupId:void 0};let Q=await N("IAM Create Group",async()=>{let K=await H0(X),Y={schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],...X.testGroup},Z=await $(`${X.scimPath}/Groups`,{method:"POST",headers:{Authorization:K,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify(Y)});z(Z.status===201,`Expected 201 Created, got ${Z.status}`);let G=m(await Z.json(),"Created SCIM group");r0(G,R2.iam.group);let W=v0(G.id,"Created group id"),H=v0(G.displayName,"Created group displayName");return q5(G,"Created SCIM group"),v2(Z,G,"Groups",W),{details:{groupId:W,displayName:H}}});return{...Q,groupId:Q.details?.groupId}}async function r$(X,$,Q){return N("IAM Delete Group",async()=>{let K=await H0(X),Y=await $(`${X.scimPath}/Groups/${Q}`,{method:"DELETE",headers:{Authorization:K}});return z(Y.status===204,`Expected 204 No Content, got ${Y.status}`),{details:{groupId:Q,deleted:!0}}})}async function s$(X,$){return N("IAM Users Endpoint Requires Authentication",async()=>{let Q=await $(`${X.scimPath}/Users`,{headers:{Accept:"application/scim+json"}});return z(Q.status===401,`Expected 401 Unauthorized for Users, got ${Q.status}`),{details:{usersRequiresAuth:!0}}})}async function f8(X,$){return N("IAM Groups Endpoint Requires Authentication",async()=>{let Q=await $(`${X.scimPath}/Groups`,{headers:{Accept:"application/scim+json"}});return z(Q.status===401,`Expected 401 Unauthorized for Groups, got ${Q.status}`),{details:{groupsRequiresAuth:!0}}})}async function e$(X,$){return N("IAM Users Endpoint Denies Unauthorized (403)",async()=>{let Q=await I8(X),K=await $(`${X.scimPath}/Users`,{headers:{Authorization:Q,Accept:"application/scim+json"}});return z(K.status===403,`Expected 403 Forbidden for an authenticated-but-unauthorized token, got ${K.status}`),{details:{deniedByDefault:!0}}})}function $9(X,$){return[{name:"IAM discovery endpoints require authentication",fn:async()=>{for(let Q of["ServiceProviderConfig","ResourceTypes","Schemas"]){let K=await $(`${X.scimPath}/${Q}`,{headers:{Accept:"application/scim+json"}});z(K.status===401,`Expected 401 from unauthenticated ${Q}, got ${K.status}`)}}},{name:"IAM ServiceProviderConfig advertises OAuth bearer auth and filtering",fn:async()=>{let Q=await $(`${X.scimPath}/ServiceProviderConfig`,{headers:{Authorization:await H0(X),Accept:"application/scim+json"}});z(Q.status===200,`Expected 200 from ServiceProviderConfig, got ${Q.status}`);let K=m(await Q.json(),"ServiceProviderConfig"),Y=m(K.filter,"ServiceProviderConfig.filter");z(Y.supported===!0,"ServiceProviderConfig must advertise filter support");let Z=$1(K.authenticationSchemes,"ServiceProviderConfig.authenticationSchemes").map((W)=>m(W,"ServiceProviderConfig authentication scheme"));z(Z.some((W)=>W.type==="oauthbearertoken"),"ServiceProviderConfig must advertise OAuth bearer token authentication");let G=m(K.meta,"ServiceProviderConfig.meta");new URL(v0(G.location,"ServiceProviderConfig.meta.location"))}},{name:"IAM Schemas exposes the ionite ECID User extension",fn:async()=>{let Q=await H0(X),K=await $(`${X.scimPath}/Schemas`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(K.status===200,`Expected 200 from Schemas, got ${K.status}`);let Y=m(await K.json(),"Schemas response");z(g1(Y.schemas,"Schemas response schemas").includes(k8),"Schemas response must be a SCIM ListResponse");let Z=$1(Y.Resources,"Schemas Resources").map((j)=>m(j,"Schema resource"));z(Z.some((j)=>j.id===P5),"Schemas must include the core User schema"),z(Z.some((j)=>j.id===R0),"Schemas must include the ionite User extension");let G=await $(`${X.scimPath}/Schemas/${encodeURIComponent(R0)}`,{headers:{Authorization:Q,Accept:"application/scim+json"}});z(G.status===200,`Expected 200 from ionite User Schema, got ${G.status}`);let W=m(await G.json(),"Ionite User Schema");z(W.id===R0,"Schema detail must identify the ionite User extension");let H=$1(W.attributes,"Ionite User Schema attributes").map((j)=>m(j,"Ionite User Schema attribute")),J=H.find((j)=>j.name==="ecid");z(J?.type==="string","Ionite User Schema must define ecid as a string"),z(J.caseExact===!0,"Ionite User Schema must define ecid as case-exact"),z(H.some((j)=>j.name==="accountClass"),"Ionite User Schema must define accountClass")}},{name:"IAM ResourceTypes binds the ionite extension to Users",fn:async()=>{let Q=await $(`${X.scimPath}/ResourceTypes`,{headers:{Authorization:await H0(X),Accept:"application/scim+json"}});z(Q.status===200,`Expected 200 from ResourceTypes, got ${Q.status}`);let K=m(await Q.json(),"ResourceTypes response"),Z=$1(K.Resources,"ResourceTypes Resources").map((W)=>m(W,"ResourceType resource")).find((W)=>W.id==="User");z(Z!==void 0,"ResourceTypes must include User");let G=$1(Z.schemaExtensions,"User schemaExtensions").map((W)=>m(W,"User schema extension"));z(G.some((W)=>W.schema===R0),"User ResourceType must reference the ionite User extension")}},{name:"IAM Users rejects a malformed ECID at the HTTP boundary",fn:async()=>{let Q=await $(`${X.scimPath}/Users`,{method:"POST",headers:{Authorization:await H0(X),"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify({schemas:[P5,R0],userName:`invalid-ecid-${Date.now()}@example.test`,active:!0,[R0]:{ecid:"not-an-ecid",accountClass:"daily"}})});z(Q.status===400,`Expected 400 for malformed ECID, got ${Q.status}`)}}]}async function p8(X){let $=performance.now(),Q={...d$,...X},K=r(Q),Y=[],Z=typeof X.ion==="function"?X.ion():X.ion;if((Z===null||Z===void 0)&&!X.ecvUrl)return{suite:"IAM",passed:!1,tests:[{name:"IAM Configuration",passed:!1,error:"IAM tests require either es (Ionite instance) or ecvUrl (ECV mock server URL)",duration:0}],duration:0,summary:{total:1,passed:0,failed:1,skipped:0}};if(Z&&!Z.workload)return{suite:"IAM",passed:!1,tests:[{name:"IAM Configuration",passed:!1,error:"Workload management is required for IAM tests. Ionite instance provided but workload is not configured.",duration:0}],duration:0,summary:{total:1,passed:0,failed:1,skipped:0}};Y.push(await s$(Q,K)),Y.push(await e$(Q,K));for(let D of $9(Q,K))Y.push(await N(D.name,async()=>{await D.fn();return}));Y.push(await c$(Q,K));let G=await n$(Q,K);if(Y.push(G),G.passed&&G.userId)Y.push(await i$(Q,K,G.userId)),Y.push(await o$(Q,K,G.userId)),Y.push(await a$(Q,K,G.userId));if(X.testGroup){Y.push(await f8(Q,K)),Y.push(await m8(Q,K));let D=await t$(Q,K);if(Y.push(D),D.passed&&D.groupId)Y.push(await r$(Q,K,D.groupId))}let W=performance.now()-$,H=Y.filter((D)=>D.passed).length,J=Y.filter((D)=>!D.passed).length,j=Y.filter((D)=>D.details?.skipped).length;return{suite:"IAM",passed:J===0,tests:Y,duration:W,summary:{total:Y.length,passed:H-j,failed:J,skipped:j}}}function F5(X){let $={...d$,...X},Q=r($),K,Y,Z;return{tests:[{name:"users endpoint requires authentication",fn:async()=>{let H=await s$($,Q);if(!H.passed)throw Error(H.error)}},{name:"users endpoint denies an authenticated-but-unauthorized token (403)",fn:async()=>{let H=await e$($,Q);if(!H.passed)throw Error(H.error)}},{name:"users endpoint returns proper SCIM schema",fn:async()=>{let H=await c$($,Q);if(!H.passed)throw Error(H.error)}},...$9($,Q),{name:"can create a user",fn:async()=>{let H=await n$($,Q);if(!H.passed)throw Error(H.error);K=H.userId}},{name:"can get a user by ID",fn:async()=>{if(!K)throw Error("No user ID from create test");let H=await i$($,Q,K);if(!H.passed)throw Error(H.error)}},{name:"can update a user",fn:async()=>{if(!K)throw Error("No user ID from create test");let H=await o$($,Q,K);if(!H.passed)throw Error(H.error)}},{name:"can delete a user",fn:async()=>{if(!K)throw Error("No user ID from create test");let H=await a$($,Q,K);if(!H.passed)throw Error(H.error)}}],ext:{createGroupsOutboundTests:()=>[{name:"can create a group in external IAM",fn:async()=>{let H=await t$($,Q);if(!H.passed)throw Error(H.error);Y=H.groupId}},{name:"can delete a group in external IAM",fn:async()=>{if(!Y)throw Error("No group ID from create test");let H=await r$($,Q,Y);if(!H.passed)throw Error(H.error)}}],createGroupsInboundTests:()=>{let H=X.groupsInboundPath??`${X.scimPath}/Groups`;return[{name:"inbound endpoint requires authentication",fn:async()=>{let J=await Q(H,{headers:{Accept:"application/scim+json"}});if(J.status!==401)throw Error(`Expected 401 Unauthorized, got ${J.status}`)}},{name:"inbound endpoint returns proper SCIM schema",fn:async()=>{let J=await H0($),j=await Q(H,{headers:{Authorization:J,Accept:"application/scim+json"}});if(j.status!==200)throw Error(`Expected 200 OK, got ${j.status}`);let D=m(await j.json(),"Inbound groups response");if(!g1(D.schemas,"Inbound response schemas").includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"))throw Error("Response missing SCIM ListResponse schema")}},{name:"can receive group creation from IAM provider",fn:async()=>{let J=await H0($),D=await Q(H,{method:"POST",headers:{Authorization:J,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"ECV Inbound Test Group",externalId:"ecv-inbound-test-group-001"})});if(D.status!==201)throw Error(`Expected 201 Created, got ${D.status}`);let P=m(await D.json(),"Created inbound SCIM group"),B=v0(P.id,"Created inbound group id");if(v0(P.displayName,"Created inbound group displayName"),q5(P,"Created inbound SCIM group"),v2(D,P,"Groups",B),!B)throw Error("Created group missing id or displayName");Z=B}},{name:"ServiceProviderConfig advertises filter support (§11.3)",fn:async()=>{let J=await H0($),j=H.replace(/\/Groups$/,""),D=await Q(`${j}/ServiceProviderConfig`,{headers:{Authorization:J,Accept:"application/scim+json"}});if(D.status!==200)throw Error(`Expected 200 OK from ServiceProviderConfig, got ${D.status}`);let P=m(await D.json(),"ServiceProviderConfig");if(m(P.filter,"ServiceProviderConfig.filter").supported!==!0)throw Error("ServiceProviderConfig must advertise filter.supported: true (filtering IS implemented)")}},{name:"externalId eq filter round-trips the created group (§11.3)",fn:async()=>{let J=await H0($),j=encodeURIComponent('externalId eq "ecv-inbound-test-group-001"'),D=await Q(`${H}?filter=${j}`,{headers:{Authorization:J,Accept:"application/scim+json"}});if(D.status!==200)throw Error(`Expected 200 OK from filtered Groups list, got ${D.status}`);let P=m(await D.json(),"Filtered groups response"),q=$1(P.Resources??[],"Filtered Resources").map((M)=>m(M,"Filtered resource")).find((M)=>M.externalId==="ecv-inbound-test-group-001");if(!q)throw Error("externalId eq filter did not return the group created with that externalId");let L=m(q.meta,"Filtered group meta"),O=v0(L.location,"Filtered group meta.location");if(!new URL(O).pathname.endsWith(`/Groups/${encodeURIComponent(v0(q.id,"Group id"))}`))throw Error("Filtered group meta.location does not address its SCIM Group resource");q5(q,"Filtered SCIM group")}},{name:"rejects a malformed PATCH body with 400 (§10.2)",fn:async()=>{if(!Z)throw Error("No group ID from create test");let J=await H0($),j=await Q(`${H}/${Z}`,{method:"PATCH",headers:{Authorization:J,"Content-Type":"application/scim+json",Accept:"application/scim+json"},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:12345}]})});if(j.status!==400)throw Error(`Expected 400 for a malformed group PATCH body, got ${j.status}`)}},{name:"can receive group deletion from IAM provider",fn:async()=>{if(!Z)throw Error("No group ID from create test");let J=await H0($),j=await Q(`${H}/${Z}`,{method:"DELETE",headers:{Authorization:J}});if(j.status!==204)throw Error(`Expected 204 No Content, got ${j.status}`)}}]}}}}import{createManagedIamDelta as c0,InMemoryAccessCatalogStore as Y9,InMemoryUserStore as Z9,ION_AUTHZ_GROUP_SCHEMA as x1,ION_SCIM_USER_EXTENSION_URN as Q9,ionite as g8,ScimOutboundClient as u8,silentLogger as h0,workload as K9}from"@ionite/server";import{createValidators as l8}from"@ionite/valibot";var d8="urn:ietf:params:scim:api:messages:2.0:ListResponse";function i(X="https://iam.example.test/scim/v2"){let $=new Map,Q=new Set,K=new Map,Y=new Set,Z=[],G=0,W,H,J=404,j=(q,L)=>new Response(JSON.stringify(L),{status:q,headers:{"Content-Type":"application/scim+json"}}),D=(q,L,O,M={})=>{let F=`${X}/${q}/${encodeURIComponent(L)}`,T=`${new URL(X).origin}/request-access/${encodeURIComponent(L)}`,x=q==="Groups"?{...M[x1]??{},requestUrl:T}:void 0;return{...M,id:L,externalId:O,meta:{resourceType:q==="Groups"?"Group":"User",location:F},...x?{[x1]:x}:{}}},P=(q,L,O)=>j(200,{schemas:[d8],totalResults:O?1:0,Resources:O?[D(q,O,L)]:[]});return{baseUrl:X,fetchImpl:async(q,L)=>{let O=new URL(q),M=(L?.method??"GET").toUpperCase(),F=O.pathname.split("/").filter(Boolean),T=F[F.length-2],x=T==="Groups"||T==="Users"?T:F[F.length-1],b=O.pathname.endsWith("/Users"),p=O.pathname.endsWith("/Groups"),F0=O.searchParams.get("filter")??void 0,k=L?.body?JSON.parse(String(L.body)):void 0;if(Z.push({method:M,path:O.pathname,filter:F0,body:k}),M==="GET"&&(b||p)){if(H!==void 0)return j(H,{detail:"lookup rejected"});let C=F0?.match(/externalId eq "([^"]*)"/i)?.[1];z(C!==void 0,"destination lookups must be by externalId filter only");let X0=(b?$:K).get(C);return P(b?"Users":"Groups",C,X0)}if(M==="POST"&&(b||p)){let C=String(k?.externalId??"");G+=1;let X0=`${b?"u":"g"}_${G}`;if(b)$.set(C,X0),Q.add(X0);else K.set(C,X0),Y.add(X0);return j(201,D(b?"Users":"Groups",X0,C,k))}if(M==="PUT"&&(x==="Users"||x==="Groups")){let C=decodeURIComponent(F[F.length-1]??"");if(!(x==="Users"?Q:Y).has(C))return j(J,{detail:"resource not found"});let u0=String(k?.externalId??"");return(x==="Users"?$:K).set(u0,C),j(200,D(x,C,u0,k))}if(M==="DELETE"&&(x==="Users"||x==="Groups")){let C=decodeURIComponent(F[F.length-1]??""),X0=x==="Users"?Q:Y,u0=x==="Users"?$:K;if(!X0.delete(C))return j(J,{detail:"resource not found"});for(let[J2,Q8]of u0)if(Q8===C)u0.delete(J2);return new Response(null,{status:204})}if(M==="PATCH"&&x==="Groups"){let C=decodeURIComponent(F[F.length-1]??"");if(!Y.has(C))return j(J,{detail:"group not found"});if(W!==void 0)return j(W,{detail:"patch rejected"});return j(200,{id:C})}return j(404,{detail:"not found"})},calls:Z,seedUser(q,L){$.set(q,L),Q.add(L)},seedGroup(q,L){K.set(q,L),Y.add(L)},countCalls(q){return Z.filter(q).length},failGroupPatchWith(q){W=q},failLookupWith(q){H=q},missingResourceWith(q){J=q}}}function X9(){let X=new Map,$=[],Q=[],K=(Y,Z)=>`${Y}\x00${Z}`;return{callbacks:{async read(Y,Z){return X.get(K(Y,Z))},async write(Y,Z,G){X.set(K(Y,Z),G),$.push({resourceType:Y,externalId:Z,reference:G})},async clear(Y,Z){X.delete(K(Y,Z)),Q.push({resourceType:Y,externalId:Z})}},get:(Y,Z)=>X.get(K(Y,Z)),set:(Y,Z,G)=>X.set(K(Y,Z),G),writes:$,clears:Q}}function L0(X,$=X9()){return{client:new u8({baseUrl:X.baseUrl,getToken:async()=>"test-token",receipts:$.callbacks,log:h0,fetchImpl:X.fetchImpl}),receipts:$}}var O0={isReady:()=>!0,ready:async()=>{return},getServerToken:async()=>"test-token",getToken:async(X)=>`token-for-${X??"server"}`};function n0(){return{userStore:new Z9,catalogStore:new Y9}}function c8(X){return{async load(){return X},async subscribe(){return},secret:{}}}function L5(){return{tests:[{name:"resolveProviderId resolves a destination id STRICTLY by externalId filter",fn:async()=>{let $=i();$.seedUser("u-alice","u_42");let{client:Q,receipts:K}=L0($),Y=await Q.resolveProviderId("user","u-alice");z(typeof Y==="object"&&Y.found==="u_42","resolves the destination id returned by the externalId filter lookup");let Z=$.calls.find((W)=>W.method==="GET"&&W.path.endsWith("/Users"));z(Z?.filter?.includes('externalId eq "u-alice"')===!0,"lookup used the externalId filter");let G=K.get("user","u-alice");z(G?.providerId==="u_42","the resolved id is written through to the row receipt")}},{name:"create-on-empty: an unknown externalId is created at the destination",fn:async()=>{let $=i(),{client:Q,receipts:K}=L0($),Y=await Q.resolveProviderId("group","g-new",{displayName:"New Group"});z(typeof Y==="object"&&Y.found.startsWith("g_"),"a missing resource is created and its id returned"),z($.countCalls((G)=>G.method==="POST"&&G.path.endsWith("/Groups"))===1,"exactly one create is issued when the destination has no match");let Z=K.get("group","g-new");z(Z?.providerId===(typeof Y==="object"?Y.found:void 0),"the provider id is cached"),z(Z?.requestUrl?.includes("/request-access/")===!0,"the IAM-owned request URL is captured from the create response")}},{name:"externalId lookup captures the IAM-owned entitlement request URL",fn:async()=>{let $=i();$.seedGroup("g-existing","g_44");let{client:Q,receipts:K}=L0($);await Q.resolveProviderId("group","g-existing");let Y=K.get("group","g-existing");z(Y?.providerId==="g_44","the lookup caches the provider id"),z(Y?.requestUrl?.endsWith("/request-access/g_44")===!0,"the lookup caches the IAM-owned request URL")}},{name:"mapping hit: a cached id avoids a second filter lookup",fn:async()=>{let $=i();$.seedGroup("g-eng","g_7");let{client:Q}=L0($);await Q.resolveProviderId("group","g-eng"),await Q.resolveProviderId("group","g-eng"),z($.countCalls((K)=>K.method==="GET"&&K.path.endsWith("/Groups"))===1,"the second resolve is served from the row receipt/process memo (no extra filter GET)")}},{name:"patchGroupMembership(add) resolves both sides by externalId and PATCHes membership",fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("g-eng","g_1");let{client:Q}=L0($);await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"});let K=$.calls.find((Z)=>Z.method==="PATCH"&&Z.path.endsWith("/Groups/g_1"));z(K!==void 0,"a membership PATCH is issued against the resolved group id");let Y=K?.body?.Operations;z(Y?.[0]?.op==="add","the patch adds membership"),z(Y?.[0]?.value?.[0]?.value==="u_1","membership references the resolved user provider id")}},{name:"stale cached group id self-heals on a destination 404 (invalidate -> re-resolve -> retry)",fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("g-eng","g_real");let{client:Q,receipts:K}=L0($);K.set("group","g-eng",{providerId:"g_stale",requestUrl:"https://old-iam.example.test/request/g_stale"}),await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"}),z($.calls.some((Z)=>Z.method==="PATCH"&&Z.path.endsWith("/Groups/g_stale")),"the first PATCH targets the stale id and is rejected (404)"),z($.calls.some((Z)=>Z.method==="PATCH"&&Z.path.endsWith("/Groups/g_real")),"after self-heal the retry targets the freshly re-resolved id");let Y=K.get("group","g-eng");z(Y?.providerId==="g_real","the row receipt is repaired to the correct provider id"),z(K.clears.some((Z)=>Z.resourceType==="group"&&Z.externalId==="g-eng"),"the stale row receipt is cleared before the externalId re-filter"),z(Y?.requestUrl?.endsWith("/request-access/g_real")===!0,"self-heal refreshes the IAM-owned request URL")}},{name:"idempotency: repeated adds do not create duplicate destination resources",fn:async()=>{let $=i(),{client:Q}=L0($),K={groupExternalId:"g-eng",userExternalId:"u-alice",op:"add",groupCreate:{displayName:"Eng"},userCreate:{userName:"alice"}};await Q.patchGroupMembership(K),await Q.patchGroupMembership(K),z($.countCalls((Y)=>Y.method==="POST"&&Y.path.endsWith("/Users"))===1,"the user is created at most once across repeated emits"),z($.countCalls((Y)=>Y.method==="POST"&&Y.path.endsWith("/Groups"))===1,"the group is created at most once across repeated emits")}},{name:"concurrent full upserts serialize without losing the later body",fn:async()=>{let $=i(),{client:Q}=L0($);await Promise.all([Q.upsertResource("group","g-eng",{displayName:"Engineering v1"}),Q.upsertResource("group","g-eng",{displayName:"Engineering v2"})]),z($.countCalls((Y)=>Y.method==="POST"&&Y.path.endsWith("/Groups"))===1,"concurrent upserts create the destination Group once");let K=$.calls.find((Y)=>Y.method==="PUT"&&Y.path.includes("/Groups/"));z(K?.body?.displayName==="Engineering v2","the later serialized upsert applies its full body")}},{name:"full entity upsert replaces a known Group with its authz extension",fn:async()=>{let $=i();$.seedGroup("g-reviewers","g_10");let{client:Q,receipts:K}=L0($),Y=await Q.upsertResource("group","g-reviewers",{schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],externalId:"g-reviewers",displayName:"Reviewers",[x1]:{type:"group",requestable:!0,requestUrl:"https://app.example.test/must-not-egress",description:"Review access",resources:["project:42"]}});z(Y==="pushed","the full Group upsert is accepted");let G=$.calls.find((H)=>H.method==="PUT"&&H.path.endsWith("/Groups/g_10"))?.body?.[x1];z(G?.requestable===!0,"the requestable flag is included in the PUT body"),z(G?.resources?.[0]==="project:42","resource bindings are included in the PUT body"),z(G?.requestUrl===void 0,"the destination-owned request URL is stripped from the PUT body");let W=K.get("group","g-reviewers");z(W?.requestUrl?.endsWith("/request-access/g_10")===!0,"the destination response refreshes the cached request URL")}},{name:"non-requestable catalog definitions and grants stay out of IAM delta",fn:async()=>{let $=i(),Q={id:"vault_secret_create",name:"Create secret",description:"Internal permission composed into requestable resource Groups",requestable:!1},Y=c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),fetchImpl:$.fetchImpl});await Y.onEntitlementUpsert(Q,"permission"),await Y.onGrant({subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test",name:"Alice",email:"alice@example.test"},subjectType:"user",targetType:"permission",target:Q}),z($.calls.length===0,"non-requestable definitions are Authz/OAA building blocks and never reach IAM delta")}},{name:"entitlement metadata upsert reapplies observed roots after the destination PUT",fn:async()=>{let $=i();$.seedGroup("reviewers","g_10");let K={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,groups:[{id:"employees",name:"Employees",description:"All employees",requestable:!0}]},Y={listByTarget:async()=>({items:[{subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test"},subjectType:"user",targetType:"group",target:K}],total:1,count:1,size:1,page:1,pages:1})};await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),accessStore:Y,fetchImpl:$.fetchImpl}).onEntitlementUpsert(K,"group");let W=$.calls.findIndex((j)=>j.method==="PUT"&&j.path.endsWith("/Groups/g_10")),H=$.calls.findIndex((j)=>j.method==="PATCH"&&j.path.endsWith("/Groups/g_10")),J=$.calls[W]?.body;z(W>=0,"the entitlement definition is replaced"),z(J?.members?.[0]?.value?.startsWith("u_")===!0,"the PUT carries the resolved existing member"),z(J?.members?.[0]?.type==="User","the PUT carries the full-sync member type"),z(J?.members?.some((j)=>j.type==="Group"&&j.value?.startsWith("g_"))===!0,"the PUT carries nested Group membership through destination provider ids"),z(H>W,"existing roots are restored after the definition PUT")}},{name:"entitlement upsert pages through every observed root (members and restore beyond one page)",fn:async()=>{let $=i();$.seedGroup("reviewers","g_10");let Q={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0},K=1002,Y=Array.from({length:K},(P,B)=>({subject:{id:`user-${B}`,externalId:`u-${B}`,userName:`user${B}@example.test`},subjectType:"user",targetType:"group",target:Q})),Z=[],G={listByTarget:async(P,B,q)=>{let L=q?.start??0,O=q?.limit??K;Z.push(L);let M=Y.slice(L,L+O);return{total:K,count:M.length,items:M,size:O,page:Math.floor(L/Math.max(O,1))+1,pages:Math.ceil(K/Math.max(O,1))}}};await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),accessStore:G,fetchImpl:$.fetchImpl}).onEntitlementUpsert(Q,"group"),z(Z.includes(1000),"the emitter requests roots beyond the first page");let j=$.calls.find((P)=>P.method==="PUT"&&P.path.endsWith("/Groups/g_10"))?.body?.members??[];z(j.length===K,"the definition PUT carries members from every page");let D=$.countCalls((P)=>P.method==="PATCH"&&P.path.endsWith("/Groups/g_10"));z(D===K,"the restore loop reapplies every observed root, not just one page")}},{name:"User deactivation and ECID egress through the managed emitter, then delete converges",fn:async()=>{let $=i(),Q=c0(h0),K=n0(),Y=Q.update({mode:"patch",url:$.baseUrl},{workload:O0,...K,fetchImpl:$.fetchImpl}),Z="u-terminated-alice",G={id:"terminated-alice",externalId:"u-terminated-alice",userName:"terminated.alice@example.test",name:"Terminated Alice",email:"terminated.alice@example.test",active:!1,ecid:"ecid:acme.test:01J9F3Z8QK7M2N4P6R8T0V2W4X",accountClass:"daily"};await K.userStore.upsert(G),await Y.onUserUpsert(G);let H=$.calls.find((j)=>j.method==="POST"&&j.path.endsWith("/Users"))?.body;z(H?.active===!1,"a terminated user egresses with active:false"),z(H?.schemas?.includes(Q9)===!0,"the User egress declares the ionite ECID extension schema");let J=H?.[Q9];z(J?.ecid===G.ecid,"the User egress carries the ECID"),z(J?.accountClass===G.accountClass,"the User egress carries accountClass"),await Y.onUserDelete("u-terminated-alice"),z($.calls.some((j)=>j.method==="DELETE"&&j.path.includes("/Users/u_")),"the managed emitter deletes the destination User by its resolved provider id"),z(await Y.getDestinationRef("user","u-terminated-alice")===void 0,"the managed emitter clears the deleted User destination reference")}},{name:"full User upsert creates on empty and delete removes the provider resource and row receipt",fn:async()=>{let $=i(),{client:Q,receipts:K}=L0($),Y=await Q.upsertResource("user","u-alice",{schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],externalId:"u-alice",userName:"alice@example.test",displayName:"Alice",active:!0,emails:[{value:"alice@example.test",primary:!0}]});z(Y==="pushed","an unknown User is created with its full body");let Z=$.calls.find((H)=>H.method==="POST"&&H.path.endsWith("/Users"));z(Z?.body?.emails?.[0]?.value==="alice@example.test","full User attributes are present on create-on-empty");let G=await Q.deleteResource("user","u-alice");z(G==="pushed","the destination User delete is accepted"),z($.calls.some((H)=>H.method==="DELETE"&&H.path.includes("/Users/u_")),"the resolved provider User is deleted");let W=K.get("user","u-alice");z(W===void 0,"the deleted User mapping is removed")}},{name:"deleteResource treats a confirmed absent destination resource as idempotent success",fn:async()=>{let $=i(),{client:Q}=L0($),K=await Q.deleteResource("group","g-absent");z(K==="pushed","confirmed absence is a successful converged delete"),z($.countCalls((Y)=>Y.method==="POST"||Y.method==="DELETE")===0,"an absent delete neither creates nor issues a provider-id DELETE")}},{name:"deleteResource self-heals a stale cached id before declaring convergence",fn:async()=>{let $=i();$.missingResourceWith(410),$.seedGroup("g-reviewers","g_real");let{client:Q,receipts:K}=L0($);K.set("group","g-reviewers",{providerId:"g_stale"});let Y=await Q.deleteResource("group","g-reviewers");z(Y==="pushed","the stale mapping converges to a successful delete"),z($.calls.some((Z)=>Z.method==="DELETE"&&Z.path.endsWith("/Groups/g_stale")),"the first delete detects the stale cached id"),z($.calls.some((Z)=>Z.method==="DELETE"&&Z.path.endsWith("/Groups/g_real")),"the client re-filters externalId and deletes the current provider id")}},{name:"direct role grants egress as membership on an extension-typed SCIM Group",fn:async()=>{let $=i();await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),fetchImpl:$.fetchImpl}).onGrant({subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test"},subjectType:"user",targetType:"role",target:{id:"reviewer",name:"Reviewer",requestable:!0}});let Y=$.calls.find((G)=>G.method==="POST"&&G.path.endsWith("/Groups")),Z=Y?.body?.[x1];z(Z?.type==="role","the role definition is represented as a typed SCIM Group"),z(Y?.body?.schemas?.includes(x1)===!0,"the typed Group extension is declared in schemas"),z(Y?.body?.externalId==="reviewer","the App-owned catalog id is the outbound SCIM externalId"),z($.calls.some((G)=>G.method==="PATCH"&&G.path.includes("/Groups/")),"the role grant produces a membership PATCH")}},{name:"workload grants resolve the workload registry and preserve the Workload member type",fn:async()=>{let $=i();await c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,workloadIdentityStore:{get:async(W)=>W==="billing-worker"?{id:W,clientId:"billing-worker-client",externalId:"w-billing-worker",displayName:"Billing worker"}:void 0},...n0(),fetchImpl:$.fetchImpl}).onGrant({subject:"billing-worker",subjectType:"workload",targetType:"role",target:{id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0}});let Y=$.calls.find((W)=>W.method==="POST"&&W.path.endsWith("/Users"));z(Y?.body?.userName==="billing-worker-client","the workload registry supplies the destination principal body");let G=$.calls.find((W)=>W.method==="PATCH")?.body?.Operations?.[0]?.value?.[0];z(G?.type==="Workload","the membership PATCH matches the full-sync Workload type")}},{name:"switching IAM destinations self-heals the same row receipt without destination-qualified mappings",fn:async()=>{let $=i("https://iam-a.example.test/scim/v2"),Q=i("https://iam-b.example.test/scim/v2");$.seedGroup("g-eng","g_at_a"),Q.seedGroup("g-eng","g_at_b");let K=X9();await L0($,K).client.resolveProviderId("group","g-eng"),z(K.get("group","g-eng")?.providerId==="g_at_a","the row initially carries destination A");let Y=await L0(Q,K).client.upsertResource("group","g-eng",{displayName:"Engineering"});z(Y==="pushed","the first write after a destination switch self-heals and succeeds"),z(Q.calls.some((Z)=>Z.method==="PUT"&&Z.path.endsWith("/Groups/g_at_a")),"the new client first observes the stale row receipt"),z(Q.calls.some((Z)=>Z.method==="GET"&&Z.path.endsWith("/Groups")&&Z.filter?.includes('externalId eq "g-eng"')),"a stale destination receipt is refreshed strictly by externalId"),z(Q.calls.some((Z)=>Z.method==="PUT"&&Z.path.endsWith("/Groups/g_at_b")),"the write retries once against destination B provider id"),z(K.get("group","g-eng")?.providerId==="g_at_b","the same row now carries destination B")}},{name:"receipt-only row write-back does not recursively egress, while deprecated updates do",fn:async()=>{let $=i(),Q=new Y9,K=new Z9,Y=globalThis.fetch;globalThis.fetch=async(G,W)=>{let H=typeof G==="string"?G:G instanceof URL?G.toString():G.url;if(H==="https://idp.example.test/token")return new Response(JSON.stringify({access_token:"test-token",token_type:"Bearer",expires_in:3600}),{status:200,headers:{"Content-Type":"application/json"}});return $.fetchImpl(H,W)};let Z=g8(c8({id:"ecv-delta-row-receipts",workload:{outgoing:{iam:{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1}}},iam:{url:$.baseUrl,delta:"patch",deltaAudience:"iam"}}),{validators:l8(),iam:{userStore:K},stores:{catalogStore:Q}});try{await Z.ready(1000),z(Z.iam?.delta?.isReady()===!0,"managed delta is ready with both receipt row stores"),await Q.groups.set({id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,source:"app",metadata:{first:1,second:2}}).ready();for(let J=0;J<100;J++){if((await Q.groups.get("reviewers"))?.providerId)break;await new Promise((j)=>setTimeout(j,5))}let G=await Q.groups.get("reviewers");z(G?.providerId?.startsWith("g_")===!0,"destination receipt is written onto the Group row"),z(G?.requestUrl?.includes("/request-access/")===!0,"destination request URL is written onto the Group row");let W=$.countCalls((J)=>(J.method==="POST"||J.method==="PUT")&&J.path.includes("/Groups"));z(W===1,"the receipt-only catalog set is suppressed instead of recursively issuing another entitlement upsert");let H={...G,metadata:{second:2,first:1},requestUrl:`${G?.requestUrl}?verified=true`};await Q.groups.set(H).ready(),await new Promise((J)=>setTimeout(J,20)),z($.countCalls((J)=>(J.method==="POST"||J.method==="PUT")&&J.path.includes("/Groups"))===1,"a receipt-only update is suppressed even when object keys are reconstructed in a different order"),await Q.groups.set({...H,deprecated:!0}).ready();for(let J=0;J<100;J++){if($.countCalls((j)=>(j.method==="POST"||j.method==="PUT")&&j.path.includes("/Groups"))>=2)break;await new Promise((j)=>setTimeout(j,5))}z($.countCalls((J)=>(J.method==="POST"||J.method==="PUT")&&J.path.includes("/Groups"))===2,"deprecated:true is a substantive row update and egresses exactly once")}finally{Z.close?.(),globalThis.fetch=Y}}},{name:"reconcile pushes definitions and users before their access roots",fn:async()=>{let $=i(),Q=c0(h0),K=n0(),Y=Q.update({mode:"patch",url:$.baseUrl},{workload:O0,...K,fetchImpl:$.fetchImpl}),Z={id:"alice",externalId:"u-alice",userName:"alice@example.test",name:"Alice",email:"alice@example.test"},G={id:"eng",name:"Engineering",description:"Engineering access",requestable:!0};await K.userStore.upsert(Z),await K.catalogStore.groups.set(G).ready();let W=await Y.reconcile({groups:[G],users:[Z],access:[{subject:Z,subjectType:"user",targetType:"group",target:G}]});z(W.pushed===3,"definition, user, and access root are all counted as pushed");let H=await K.catalogStore.groups.get("eng"),J=await K.userStore.get("alice");z(H?.providerId?.startsWith("g_")===!0,"first Group resolution persists providerId on row"),z(H?.requestUrl?.includes("/request-access/")===!0,"first Group resolution persists IAM requestUrl on row"),z(J?.providerId?.startsWith("u_")===!0,"first User resolution persists providerId on row");let j=$.calls.findIndex((O)=>O.method==="POST"&&O.path.endsWith("/Groups")),D=$.calls.findIndex((O)=>O.method==="POST"&&O.path.endsWith("/Users")),P=$.calls.findIndex((O)=>O.method==="PATCH");z(j>=0&&j<P,"Group definition is pushed before membership"),z(D>=0&&D<P,"User definition is pushed before membership");let B=await Y.getDestinationRef("group","eng");z(B?.providerId.startsWith("g_")===!0,"the emitter exposes the captured provider id"),z(B?.requestUrl?.includes("/request-access/")===!0,"the emitter exposes the IAM-owned request URL without another network lookup");let q=$.countCalls((O)=>O.method==="GET"),L=await Y.reconcile({groups:[G],users:[Z]});z(L.errors.length===0,"later full-body pushes succeed using persisted row receipts"),z($.countCalls((O)=>O.method==="GET")===q,"later Group and User pushes reuse row receipts without another externalId lookup")}},{name:"readiness: patch needs url + workload + UserStore + AccessCatalogStore",fn:async()=>{let $=c0(h0),Q=n0(),K=$.update({mode:"patch"},{workload:O0,...Q});z(!K.isReady(),"'patch' without a destination url is not ready");let Y=await K.reconcile({});z(Y.errors[0]==="iam delta not ready: destination URL is not configured","an attempted operation reports the failed readiness precondition");let Z=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},Q);z(!Z.isReady(),"'patch' without a workload identity is not ready");let G=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,catalogStore:Q.catalogStore});z(!G.isReady(),"'patch' without a UserStore cannot persist User receipts");let W=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,userStore:Q.userStore});z(!W.isReady(),"'patch' without an AccessCatalogStore cannot persist Group receipts");let H=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,...Q});z(H.isReady(),"'patch' with url + workload + both receipt row stores is ready"),await H.ready(1000);let J=K9(void 0,h0,{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1});z(typeof J?.isReady==="function"&&typeof J.ready==="function","the raw configured workload runtime exposes readiness"),z(J.isReady(),"the raw configured workload runtime is ready"),await J.ready(1000);let j=$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:J,...Q});z(j.isReady(),"IAM delta accepts the workload runtime built during config apply");let D=K9(void 0,h0,{outgoing:{iam:{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1}}});z(typeof D?.isReady==="function"&&typeof D.ready==="function"&&"iam"in D,"the aggregate workload runtime exposes module and named-client readiness"),z(D.isReady(),"the aggregate workload runtime is ready");let P=$.update({mode:"patch",url:"https://iam.example.test/scim/v2",audience:"iam"},{workload:D,...Q});z(P.isReady(),"IAM delta accepts the aggregate workload runtime and named outgoing client used by vault tenants");let B=$.update({mode:void 0,url:"https://iam.example.test/scim/v2"},{workload:O0,...Q});z(!B.isReady(),"removing delta mode de-configures the stable handle");let q=!1;try{await B.ready(1)}catch{q=!0}z(q,"ready() re-arms while IAM delta is de-configured"),$.update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,...Q}),await B.ready(1000)}},{name:"emitter skips an access root whose subject or target cannot be resolved",fn:async()=>{let Q=c0(h0).update({mode:"patch",url:"https://iam.example.test/scim/v2"},{workload:O0,...n0()});z(Q.isReady(),"emitter is ready for the reconcile path");async function*K(){yield{subject:"alice",subjectType:"user",targetType:"group",target:"eng",status:"active"}}let Y=await Q.reconcile({access:K()});z(Y.pushed===0,"an unresolvable subject/target is not pushed"),z(Y.skipped===1,"the unresolved root is skipped"),z(Y.errors.length===0,"a skip is not an error")}},{name:'patchGroupMembership reports "error" when the destination rejects the PATCH (no false success)',fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("g-eng","g_1"),$.failGroupPatchWith(500);let{client:Q}=L0($),K=await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"});z(K==="error",'a non-2xx destination PATCH is reported as an error, not "pushed"')}},{name:'patchGroupMembership reports "error" (never a create) when the externalId lookup fails',fn:async()=>{let $=i();$.failLookupWith(500);let{client:Q}=L0($),K=await Q.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add",groupCreate:{displayName:"Eng"},userCreate:{userName:"alice"}});z(K==="error",'a failed lookup is reported as an error, not "skipped" or "pushed"'),z($.countCalls((Y)=>Y.method==="POST")===0,"a failed lookup must NOT fall through to create-on-empty (no POST issued)")}},{name:"reconcile surfaces a failing destination PATCH as a reconcile error",fn:async()=>{let $=i();$.seedUser("u-alice","u_1"),$.seedGroup("eng","g_1"),$.failGroupPatchWith(503);let K=c0(h0).update({mode:"patch",url:$.baseUrl},{workload:O0,...n0(),fetchImpl:$.fetchImpl});async function*Y(){yield{subject:{id:"alice",externalId:"u-alice",userName:"alice"},subjectType:"user",targetType:"group",target:{id:"eng",displayName:"Eng",requestable:!0},status:"active"}}let Z=await K.reconcile({access:Y()});z(Z.pushed===0,"a rejected destination push is not counted as pushed"),z(Z.errors.length>0,"a rejected destination PATCH is recorded as a reconcile error")}}]}}async function n8(){let X=performance.now(),$=[],Q=[...L5().tests];for(let G of Q)$.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=$.filter((G)=>!G.passed).length,Z=$.filter((G)=>G.details?.skipped).length;return{suite:"IAM Delta",passed:Y===0,tests:$,duration:K,summary:{total:$.length,passed:$.length-Y-Z,failed:Y,skipped:Z}}}import{ionite as D0}from"@ionite/server";import{createValidators as B0}from"@ionite/valibot";var w1=(X)=>new Promise(($)=>setTimeout($,X));async function C0(X,$=1500){let Q=Date.now()+$;while(Date.now()<Q){if(X())return!0;await w1(10)}return X()}function P0(X,$,Q={}){let K=X,Y,Z=0,G=new Map(Object.entries($)),W=new Map,H=new Map,J=new Map,j=new Map,D=[],P=[],B=[],q=new Set,L=(F)=>{let T=G.get(F);if(T===void 0)throw Error(`Missing test secret ${F}`);return{data:structuredClone(T),metadata:{path:F,version:Z,created:new Date(0)}}},O=(F)=>{for(let T of W.get(F)??[])T(L(F))};return{secret:{type:"dev",isReady:()=>!0,ready:async()=>{},isAvailable:()=>!0,get:()=>{throw Error("not implemented")},getFullSecret:async(F)=>{return J.set(F,(J.get(F)??0)+1),L(F)},getSecret:async(F)=>{return J.set(F,(J.get(F)??0)+1),L(F).data},putSecret:async()=>{throw Error("not implemented")},deleteSecret:async()=>{throw Error("not implemented")},listPaths:async()=>[],exists:async(F)=>G.has(F),requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async(F,T)=>{return D.push({path:F,request:{...T}}),{parked:!0,disposition:T.status===403||T.status===401?"suspect":"cooldown"}},addKmsKeys:async(F,T)=>{return P.push({path:F,request:{keys:T.keys.map((x)=>({...x}))}}),{keys:T.keys.map((x,b)=>({id:x.id??`issued-${b+1}`}))}},revokeKmsKey:async(F,T)=>{return B.push({path:F,request:{...T}}),{revoked:!0}},...Q.events?{subscribeEvents:(F)=>{return q.add(F),()=>q.delete(F)}}:{},getMetadata:async()=>({}),subscribe:async(F,T,x)=>{H.set(F,(H.get(F)??0)+1),j.set(F,{...x?.kmsSlots===void 0?{}:{kmsSlots:x.kmsSlots}});let b=W.get(F)??new Set;if(b.add(T),W.set(F,b),!Q.delayInitialDelivery)T(L(F));return()=>b.delete(T)}},async load(){return K},async subscribe(F){return Y=F,()=>{Y=void 0}},pushConfig(F){K=F,Y?.(F)},pushPool(F,T){Z+=1,G.set(F,structuredClone(T)),O(F)},deliverPool(F){O(F)},subscriptionCount(F){return H.get(F)??0},fetchCount(F){return J.get(F)??0},lastSubscribeOptions(F){return j.get(F)},parkCalls(){return D},issueCalls(){return P},revokeCalls(){return B},emitEvent(F){for(let T of q)T(F)}}}function q0(X,$,Q){return{kind:"kms",slots:[X],keys:[{id:X,state:"active",value:$,notBefore:new Date(0).toISOString()}],...Q}}async function i0(X,$){let Q=!1;try{await X}catch{Q=!0}z(Q,$)}function V5(){return{tests:[{name:"kms selects the active key and preserves graceful-rotation overlap until removal or revocation",fn:async()=>{let $=P0({id:"kms-rotation",kms:{rotation:{path:"/kms/rotation"}}},{["/kms/rotation"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("rotation");z(K?.isReady()===!0,"Expected configured KMS pool to become ready"),w(await K.value(),"token-1","Expected value() to select the declared active key"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k2"],keys:[{id:"k1",state:"deprecated",value:"token-1",notAfter:new Date(Date.now()+60000).toISOString()},{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),w(await K.value(),"token-2","Expected rotation to select the new active key"),z(K.accepts("token-1"),"Expected the deprecated overlap key to remain accepted"),z(K.accepts("token-2"),"Expected the new active key to be accepted"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k2"],keys:[{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),z(!K.accepts("token-1"),"Expected a removed overlap key to stop being accepted immediately"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k3"],keys:[{id:"k2",state:"deprecated",value:"token-2",notAfter:new Date(Date.now()+60000).toISOString()},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),z(K.accepts("token-2"),"Expected the second graceful overlap key to remain accepted"),$.pushPool("/kms/rotation",{kind:"kms",slots:["k3"],keys:[{id:"k2",state:"revoked"},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),z(!K.accepts("token-2"),"Expected a revoked overlap key to stop being accepted immediately"),w(await K.value(),"token-3","Expected revocation not to disturb the current active key")}finally{Q.close()}}},{name:"kms hard replacement drops the old key immediately",fn:async()=>{let $=P0({id:"kms-hard-replacement",kms:{hard:{path:"/kms/hard-replacement"}}},{["/kms/hard-replacement"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("hard");z(K?.isReady()===!0,"Expected the initial hard-replacement pool grant"),z(K.accepts("token-1"),"Expected the initial key to be accepted"),$.pushPool("/kms/hard-replacement",q0("k2","token-2")),w(await K.value(),"token-2","Expected hard replacement to select the replacement key"),z(!K.accepts("token-1"),"Expected hard replacement to drop the old key immediately"),z(K.accepts("token-2"),"Expected hard replacement to accept the replacement key")}finally{Q.close()}}},{name:"kms empty grants re-arm value waiting and a later grant resolves the pending call",fn:async()=>{let $=P0({id:"kms-empty-grant",kms:{empty:{path:"/kms/empty-grant"}}},{["/kms/empty-grant"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("empty");z(K?.isReady()===!0,"Expected the initial pool grant"),$.pushPool("/kms/empty-grant",{kind:"kms",keys:[]}),z(await C0(()=>!K.isReady()),"Expected an empty grant to make the pool not ready"),z(!K.accepts("token-1"),"Expected an empty grant to revoke the cached key"),await i0(K.value(30),"Expected value(timeout) to reject when the empty grant is not replaced");let Y=!1,Z=K.value().then((G)=>{return Y=!0,G});await w1(20),z(!Y,"Expected value() to remain pending while the grant is empty"),$.pushPool("/kms/empty-grant",q0("k2","token-2")),w(await Z,"token-2","Expected a later grant to resolve the already-pending value()"),z(K.isReady(),"Expected the pool to become ready after the later grant")}finally{Q.close()}}},{name:"kms concurrent first value calls wait for initial delivery through one source subscription",fn:async()=>{let $=P0({id:"kms-delayed-initial",kms:{delayed:{path:"/kms/delayed-initial"}}},{["/kms/delayed-initial"]:q0("k1","token-1")},{delayInitialDelivery:!0}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("delayed");z(K!==void 0,"Expected the delayed pool handle"),z(!K.isReady(),"Expected the pool to wait for its initial grant delivery");let Y=Array.from({length:8},()=>K.value());z(Y.every((G)=>G===Y[0]),"Expected all concurrent value() calls to share the same pending waiter");let Z=!1;Y[0]?.then(()=>{Z=!0}),await w1(20),z(!Z,"Expected the first value() to wait before initial delivery"),w($.subscriptionCount("/kms/delayed-initial"),1,"Expected all concurrent callers to share one source subscription"),w($.fetchCount("/kms/delayed-initial"),0,"Expected concurrent value() calls not to issue duplicate fetches"),$.deliverPool("/kms/delayed-initial"),w((await Promise.all(Y)).join(","),Array.from({length:8},()=>"token-1").join(","),"Expected the initial delivery to resolve every concurrent caller with the same grant"),w($.subscriptionCount("/kms/delayed-initial"),1,"Expected initial delivery not to create a new subscription"),w($.fetchCount("/kms/delayed-initial"),0,"Expected initial delivery to avoid one-off source fetches")}finally{Q.close()}}},{name:"kms cached repeated value calls create no additional source calls",fn:async()=>{let $=P0({id:"kms-cached",kms:{cached:{path:"/kms/cached"}}},{["/kms/cached"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("cached");z(K?.isReady()===!0,"Expected the cached pool grant");let Y=$.subscriptionCount("/kms/cached"),Z=$.fetchCount("/kms/cached"),G=await Promise.all(Array.from({length:20},()=>K.value()));z(G.every((W)=>W==="token-1"),"Expected every cached call to return the active key"),w($.subscriptionCount("/kms/cached"),Y,"Expected cached value() calls not to add subscriptions"),w($.fetchCount("/kms/cached"),Z,"Expected cached value() calls not to fetch")}finally{Q.close()}}},{name:"kms value waits for a future notBefore boundary",fn:async()=>{let $=P0({id:"kms-not-before",kms:{scheduled:{path:"/kms/not-before"}}},{["/kms/not-before"]:q0("initial","initial-token")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("scheduled");z(K!==void 0,"Expected the scheduled pool handle"),z(K.isReady(),"Expected the initial pool grant");let Y=new Date(Date.now()+150).toISOString();$.pushPool("/kms/not-before",q0("future","future-token",{keys:[{id:"future",state:"active",value:"future-token",notBefore:Y}]})),z(!K.isReady(),"Expected the future notBefore key to remain unavailable initially");let Z=!1,G=K.value(1000).then((W)=>{return Z=!0,W});await w1(30),z(!Z,"Expected value() to wait before the notBefore boundary"),w(await G,"future-token","Expected value() to resolve when notBefore becomes current"),z(K.isReady(),"Expected the pool to become ready at the notBefore boundary")}finally{Q.close()}}},{name:"kms de-configuration rejects a pending value and re-arms the stable pool handle",fn:async()=>{let $=P0({id:"kms-rearm",kms:{rearm:{path:"/kms/rearm"}}},{["/kms/rearm"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("rearm");z(K?.isReady()===!0,"Expected the initial pool grant"),$.pushPool("/kms/rearm",{kind:"kms",keys:[]}),z(await C0(()=>!K.isReady()),"Expected the empty grant to re-arm value waiting");let Y=K.value().then(()=>"resolved",()=>"rejected");$.pushConfig({id:"kms-rearm"}),z(await C0(()=>!K.isReady()),"Expected the pool to remain not ready after removal"),w(await Promise.race([Y,w1(300).then(()=>"timed-out")]),"rejected","Expected de-configuration to reject the pending value() call"),await i0(K.ready(30),"Expected the re-armed pool ready(timeout) to reject while de-configured");let Z=!1,G=K.ready().then(()=>{Z=!0});await w1(20),z(!Z,"Expected the re-armed ready() waiter to stay pending while de-configured"),$.pushPool("/kms/rearm",q0("k2","token-2")),$.pushConfig({id:"kms-rearm",kms:{rearm:{path:"/kms/rearm"}}}),z(await C0(()=>K.isReady()),"Expected the same stable pool handle to recover after re-configuration"),await G,z(Z,"Expected the re-armed ready() waiter to resolve after re-configuration"),w(await K.value(),"token-2","Expected the restored handle to expose the later grant")}finally{Q.close()}}},{name:"kms wrong document kind and shape fail closed until value timeout or a valid grant",fn:async()=>{let $=P0({id:"kms-invalid",kms:{invalid:{path:"/kms/invalid"}}},{["/kms/invalid"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("invalid");z(K?.isReady()===!0,"Expected the initial valid grant"),$.pushPool("/kms/invalid",{kind:"token",slots:["wrong-kind"],keys:[{id:"wrong-kind",state:"active",value:"wrong-token"}]}),z(await C0(()=>!K.isReady()),"Expected a wrong document kind to fail closed"),z(!K.accepts("token-1"),"Expected invalid delivery to clear previously accepted keys");let Y=performance.now();await i0(K.value(40),"Expected value(timeout) to reject while a wrong-kind document remains current"),z(performance.now()-Y>=20,"Expected wrong-kind value() to wait for its timeout instead of rejecting immediately"),$.pushPool("/kms/invalid",{kind:"kms",slots:["missing"],keys:[]}),z(await C0(()=>!K.isReady()),"Expected an invalid KMS document shape to fail closed");let Z=performance.now();await i0(K.value(40),"Expected value(timeout) to reject while an invalid-shape document remains current"),z(performance.now()-Z>=20,"Expected invalid-shape value() to wait for its timeout instead of rejecting immediately"),$.pushPool("/kms/invalid",q0("k2","token-2")),w(await K.value(100),"token-2","Expected a later valid grant to recover the failed-closed pool")}finally{Q.close()}}},{name:"otel apiKey and apiKeyPool are rejected when configured together",fn:async()=>{let $=P0({id:"kms-conflict",kms:{conflict:{path:"/kms/conflict"}},otel:{name:"kms-conflict",apiKey:"static",apiKeyPool:"conflict",traces:!1,metrics:!1,logs:!1}},{["/kms/conflict"]:q0("k1","managed")}),Q=D0($,{validators:B0()});try{await i0(Q.ready(500),"Expected mutually exclusive OTel credential sources to reject config apply"),z(Q.configStatus.lastError!==void 0,"Expected configStatus.lastError for invalid reference")}finally{Q.close()}}},{name:"kms unique slots stay distinct, alias slot 0, and raise demand on one resubscribe",fn:async()=>{let $=P0({id:"kms-unique",kms:{unique:{path:"/kms/unique"}}},{["/kms/unique"]:{kind:"kms",slots:["k1"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()}]}}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("unique");z(K!==void 0,"Expected the unique pool handle"),w($.lastSubscribeOptions("/kms/unique")?.kmsSlots,1,"Expected the default subscribe demand to be 1"),w(K.current(),"token-1","Expected current() to read the cached slot-0 grant");let Y=K.unique(3),Z=K.unique(3);z(Y.length===3&&Y.every((H,J)=>H===Z[J]),"Expected repeated unique(n) calls to return the same slot handles"),z(Y[0]!==K,"Expected unique() slot handles to be distinct from the pool object"),w(await Y[0]?.value(),"token-1","Expected unique()[0] to alias the pool slot-0 grant"),z(Y[1]?.isReady()===!1,"Expected unfilled slot 1 to stay not ready"),z(Y[2]?.isReady()===!1,"Expected unfilled slot 2 to stay not ready"),w(K.current(),"token-1","Expected raising demand not to clear the slot-0 cache"),await i0(Y[1]?.ready(30),"Expected unfilled slot ready(timeout) to reject"),w($.subscriptionCount("/kms/unique"),2,"Expected unique(3) to resubscribe exactly once"),w($.lastSubscribeOptions("/kms/unique")?.kmsSlots,3,"Expected the resubscribe to carry demand 3");let G=!1,W=Y[1]?.value().then((H)=>{return G=!0,H});await w1(20),z(!G,"Expected an unfilled unique() slot to wait for a later grant"),$.pushPool("/kms/unique",{kind:"kms",slots:["k1","k2","k3"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()},{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),w(await W,"token-2","Expected a later multi-slot grant to fill waiting slots"),w(await Y[2]?.value(),"token-3","Expected slot 2 to receive a distinct key"),w(await K.value(),"token-1","Expected pool.value() to keep aliasing slot 0"),z(K.accepts("token-1")&&K.accepts("token-2")&&K.accepts("token-3"),"Expected accepts() to span every delivered slot"),w(Y[1]?.current(),"token-2","Expected slot current() after the grant"),w($.subscriptionCount("/kms/unique"),2,"Expected later unique(2) not to resubscribe after demand 3"),z(K.unique(2)[0]===Y[0]&&K.unique(2)[1]===Y[1],"Expected a later smaller unique(n) to reuse the existing handles")}finally{Q.close()}}},{name:"kms current() is undefined before a grant and after de-configuration",fn:async()=>{let $=P0({id:"kms-current",kms:{current:{path:"/kms/current"}}},{["/kms/current"]:q0("k1","token-1")},{delayInitialDelivery:!0}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("current");z(K!==void 0,"Expected the current() pool handle"),z(K.current()===void 0,"Expected current() to be undefined before the first grant"),$.deliverPool("/kms/current"),z(await C0(()=>K.current()==="token-1"),"Expected current() after grant delivery"),$.pushConfig({id:"kms-current"}),z(await C0(()=>K.current()===void 0),"Expected current() to clear on de-configuration")}finally{Q.close()}}},{name:"kms park() resolves the used value to a key id and never sends the secret",fn:async()=>{let $=P0({id:"kms-park",kms:{park:{path:"/kms/park"}}},{["/kms/park"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("park");z(K!==void 0,"Expected the park pool handle"),z(await C0(()=>K.current()==="token-1"),"Expected the grant before park");let Y=await K.park({value:"token-1",status:429,reason:"rate limited"});w(Y.parked,!0,"Expected a successful park"),w(Y.disposition,"cooldown","Expected 429 to map to cooldown on the fake source"),w($.parkCalls().length,1,"Expected one outbound park"),w($.parkCalls()[0]?.path,"/kms/park","Expected park to use the bound path"),w($.parkCalls()[0]?.request.keyId,"k1","Expected value→id resolution"),w($.parkCalls()[0]?.request.status,429,"Expected status to be forwarded"),w($.parkCalls()[0]?.request.reason,"rate limited","Expected reason to be forwarded"),z(!JSON.stringify($.parkCalls()[0]?.request).includes("token-1"),"Expected the used value never to appear on the outbound request");let Z=await K.park({value:"already-rotated"});w(Z.parked,!1,"Expected a stale value not to park"),w(Z.stale,!0,"Expected stale to be marked"),w($.parkCalls().length,1,"Expected a stale value not to call the source");let W=await K.unique(1)[0]?.park({value:"token-1",status:403});w(W?.parked,!0,"Expected slot.park to alias the pool"),w($.parkCalls()[1]?.request.keyId,"k1","Expected slot park to resolve the same key"),w($.parkCalls()[1]?.request.status,403,"Expected 403 to be forwarded from the slot alias")}finally{Q.close()}}},{name:"kms park() fails fast when the pool is not configured",fn:async()=>{let $=P0({id:"kms-park-unconfigured",kms:{park:{path:"/kms/park-unconfigured"}}},{["/kms/park-unconfigured"]:q0("k1","token-1")}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("park");z(K!==void 0,"Expected the park pool handle"),z(await C0(()=>K.current()==="token-1"),"Expected the grant before park");let Y="";try{await K.park({value:""})}catch(G){Y=G instanceof Error?G.message:String(G)}z(Y.includes("requires a non-empty value"),"Expected empty-value park() to fail fast on the configured handle"),w($.parkCalls().length,0,"Expected empty-value park() not to call the source"),$.pushConfig({id:"kms-park-unconfigured"}),z(await C0(()=>!K.isReady()),"Expected the pool handle to tombstone");let Z="";try{await K.park({value:"token-1"})}catch(G){Z=G instanceof Error?G.message:String(G)}z(Z.includes("is not configured"),"Expected tombstoned park() to reject on the SDK handle"),w($.parkCalls().length,0,"Expected tombstoned park() not to call the source")}finally{Q.close()}}},{name:"kms issuer addKeys and revokeKey forward and fail fast when unconfigured or empty",fn:async()=>{let $=P0({id:"kms-issuer",kms:{issuer:{path:"/kms/issuer"}}},{["/kms/issuer"]:{kind:"kms",keys:[]}}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.issuer("issuer");z(K!==void 0,"Expected the issuer handle"),z(K.isReady(),"Expected issuer readiness to be independent of slot grants");let Y=await K.addKeys([{value:"oem-token"}]);w(Y.keys[0]?.id,"issued-1","Expected the source to assign a key id"),w($.issueCalls().length,1,"Expected one outbound addKeys"),w($.issueCalls()[0]?.request.keys[0]?.value,"oem-token","Expected the issued value");let Z=await K.revokeKey({keyId:"issued-1",reason:"compromise"});w(Z.revoked,!0,"Expected revoke to succeed"),w($.revokeCalls()[0]?.request.keyId,"issued-1","Expected revoke to forward the key id"),await i0(K.addKeys([]),"Expected empty addKeys() to fail fast"),await i0(K.revokeKey({keyId:""}),"Expected empty-keyId revokeKey() to fail fast"),w($.issueCalls().length,1,"Expected empty addKeys() not to call the source"),w($.revokeCalls().length,1,"Expected empty revokeKey() not to call the source"),$.pushConfig({id:"kms-issuer"}),z(await C0(()=>!K.isReady()),"Expected issuer handle to tombstone"),await i0(K.addKeys([{value:"oem-token"}]),"Expected tombstoned addKeys() to reject"),w($.issueCalls().length,1,"Expected tombstoned addKeys() not to call the source")}finally{Q.close()}}},{name:"kms issuer events() filters kms_* events and errors when the transport lacks subscribeEvents",fn:async()=>{let $=P0({id:"kms-issuer-events",kms:{events:{path:"/kms/issuer-events"}}},{["/kms/issuer-events"]:{kind:"kms",keys:[]}},{events:!0}),Q=D0($,{validators:B0()});try{await Q.ready(500);let Z=Q.kms?.issuer("events");z(Z!==void 0,"Expected the issuer handle");let G=[],W=Z.events((H)=>{G.push(`${H.event}:${H.keyId??""}`)});$.emitEvent({path:"/kms/issuer-events",event:"updated"}),$.emitEvent({path:"/other",event:"kms_key_parked",keyId:"other"}),$.emitEvent({path:"/kms/issuer-events",event:"kms_key_parked",keyId:"k1"}),$.emitEvent({path:"/kms/issuer-events",event:"kms_pool_low"}),w(G.join(","),"kms_key_parked:k1,kms_pool_low:","Expected filtered issuer events"),W()}finally{Q.close()}let K=P0({id:"kms-issuer-no-events",kms:{events:{path:"/kms/issuer-events"}}},{["/kms/issuer-events"]:{kind:"kms",keys:[]}}),Y=D0(K,{validators:B0()});try{await Y.ready(500);let Z=Y.kms?.issuer("events");z(Z!==void 0,"Expected the issuer handle");let G="";try{Z.events(()=>{})}catch(W){G=W instanceof Error?W.message:String(W)}z(G.includes("events are not supported by this secrets transport"),"Expected events() to fail when the source lacks subscribeEvents")}finally{Y.close()}}},{name:"kms verifier grants parse without slots, accept keys, and reject value/unique",fn:async()=>{let $=P0({id:"kms-verifier",kms:{verifier:{path:"/kms/verifier"}}},{["/kms/verifier"]:{kind:"kms",keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()},{id:"k2",state:"deprecated",value:"token-2",notAfter:new Date(Date.now()+60000).toISOString()}]}}),Q=D0($,{validators:B0()});try{await Q.ready(500);let K=Q.kms?.getPool("verifier");z(K!==void 0,"Expected the verifier pool handle"),z(await C0(()=>K.isReady()),"Expected a verifier grant with usable keys to be ready"),z(K.accepts("token-1"),"Expected the active verifier key to be accepted"),z(K.accepts("token-2"),"Expected the overlap-deprecated verifier key to be accepted"),z(!K.accepts("token-other"),"Expected an unknown value to be rejected"),z(K.current()===void 0,"Expected current() to stay undefined on a verifier grant"),await i0(K.value(30),"Expected value() to reject on a verifier grant");let Y="";try{K.unique(1)}catch(Z){Y=Z instanceof Error?Z.message:String(Z)}z(Y.includes("verifier grant has no slots"),"Expected unique() to reject")}finally{Q.close()}}}]}}async function i8(){let X=performance.now(),$=[];for(let Y of V5().tests)$.push(await N(Y.name,async()=>{await Y.fn();return}));let Q=$.filter((Y)=>!Y.passed).length,K=$.filter((Y)=>Y.details?.skipped).length;return{suite:"KMS",passed:Q===0,tests:$,duration:performance.now()-X,summary:{total:$.length,passed:$.length-Q-K,failed:Q,skipped:K}}}var Q1="0.0.23";var U2=">=0.0.23-beta.20260809.2 <0.1.0",G9=[];function z9(X){let $=X.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);return z($!==null,`Invalid semantic version: ${X}`),{major:Number($[1]),minor:Number($[2]),patch:Number($[3]),prerelease:$[4]?$[4].split(".").map((Q)=>/^\d+$/.test(Q)?Number(Q):Q):[]}}function o8(X,$){if(X===$)return 0;if(typeof X==="number"&&typeof $==="string")return-1;if(typeof X==="string"&&typeof $==="number")return 1;return X<$?-1:1}function a8(X,$){let Q=z9(X),K=z9($);for(let Z of["major","minor","patch"])if(Q[Z]!==K[Z])return Q[Z]<K[Z]?-1:1;if(Q.prerelease.length===0&&K.prerelease.length===0)return 0;if(Q.prerelease.length===0)return 1;if(K.prerelease.length===0)return-1;let Y=Math.max(Q.prerelease.length,K.prerelease.length);for(let Z=0;Z<Y;Z++){let G=Q.prerelease[Z],W=K.prerelease[Z];if(G===void 0)return-1;if(W===void 0)return 1;let H=o8(G,W);if(H!==0)return H}return 0}function t8(X,$){let Q=$.match(/^(>=|<=|>|<|=)?(.+)$/);z(Q!==null,`Invalid semantic version comparator: ${$}`);let K=a8(X,Q[2]);switch(Q[1]??"="){case">=":return K>=0;case"<=":return K<=0;case">":return K>0;case"<":return K<0;default:return K===0}}function W9(X,$=U2){return $.split(/\s+/).filter(Boolean).every((Q)=>t8(X,Q))}function H9(X){return`ECV(meta.run) ecv=${Q1} catalog=${X}`}function J9(X,$,Q){z(W9(X.sdk.version),`Runtime SDK ${X.sdk.version} is not compatible with ECV ${Q1} (${U2})`),z(!G9.includes(X.sdk.version),`Runtime SDK ${X.sdk.version} is not accepted by ECV ${Q1}; upgrade the SDK`),Q?.annotate?.(X.sdk.version,"ecv.sdk"),Q?.annotate?.(Q1,"ecv.version"),Q?.annotate?.(String(X.sdk.catalogVersion),"ecv.catalog"),Q?.annotate?.([...$].sort().join(","),"ecv.claims")}function j9(X,$,Q){let K=new Set($),Y=$.filter((G)=>X.capabilities[G]?.testable!==!0);z(Y.length===0,`Claimed capabilities are not testable: ${Y.map((G)=>`${G} (${X.capabilities[G]?.reason??"no manifest state"})`).join("; ")}`);let Z=Object.keys(X.capabilities).filter((G)=>X.capabilities[G].testable&&!K.has(G));if(Z.length>0)Q?.annotate?.(`Runtime capabilities not claimed by this suite: ${Z.join(",")}`,"ecv.unclaimed")}import{createServer as r8}from"node:http";import{ionite as u1}from"@ionite/server";import{createValidators as l1}from"@ionite/valibot";var b2=(X)=>new Promise(($)=>setTimeout($,X));async function D1(X,$=1500,Q=20){let K=Date.now()+$;while(Date.now()<K){if(X())return!0;await b2(Q)}return X()}async function s8(){let X=[],$=r8((K,Y)=>{let Z=[];K.on("data",(G)=>Z.push(G)),K.on("end",()=>{X.push({path:K.url??"",headers:K.headers,body:Buffer.concat(Z)}),Y.statusCode=200,Y.end()})});await new Promise((K,Y)=>{$.once("error",Y),$.listen(0,"127.0.0.1",K)});let Q=$.address();if(!Q||typeof Q==="string")throw Error("OTLP recorder did not bind a TCP port");return{endpoint:`http://127.0.0.1:${Q.port}`,requests:X,close:()=>new Promise((K,Y)=>$.close((Z)=>Z?Y(Z):K()))}}function N5(X){let $=X,Q;return{async load(){return $},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){$=K,Q?.(K)},secret:{}}}function e8(X){let Q=X,K=1,Y=new Set,Z=()=>({data:structuredClone(Q),metadata:{path:"/kms/otel-ecv",version:K,created:new Date(0)}});return{secret:{type:"dev",isReady:()=>!0,ready:async()=>{},get:()=>{throw Error("not implemented")},getFullSecret:async()=>Z(),getSecret:async()=>structuredClone(Q),putSecret:async()=>{},deleteSecret:async()=>{},listPaths:async()=>[],exists:async()=>!0,requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async()=>{throw Error("parkKmsKey is not implemented for this test source")},addKmsKeys:async()=>{throw Error("addKmsKeys is not implemented for this test source")},revokeKmsKey:async()=>{throw Error("revokeKmsKey is not implemented for this test source")},getMetadata:async()=>({}),subscribe:async(W,H)=>{return Y.add(H),H(Z()),()=>Y.delete(H)}},async load(){return{id:"otel-kms",kms:{"otel-ecv":{path:"/kms/otel-ecv"}},otel:{...y2,apiKeyPool:"otel-ecv"}}},async subscribe(){return()=>{}},pushPool(W){Q=W,K+=1;for(let H of Y)H(Z())}}}var y2={name:"ecv-otel",traces:!1,metrics:!1,logs:!1};function $Q(){return{name:"OTLP logs use HTTP/protobuf by default and preserve configured exporter headers",fn:async()=>{let X=await s8(),$=N5({id:"otel-protobuf-wire",otel:{name:"ecv-otel-wire",endpoint:X.endpoint,headers:{"x-ecv-api-key":"wire-secret"},traces:!1,metrics:!1,logs:!0}}),Q=u1($,{validators:l1(),otel:{console:!1}});try{await Q.ready(1000),z(await D1(()=>Q.otel?.isReady?.()===!0),"Expected OTel ready before emitting a log"),Q.otel?.logger.info("ecv protobuf wire marker",{"ecv.marker":"protobuf"}),await Q.otel?.forceFlush?.(),z(await D1(()=>X.requests.length>0),"Expected the OTLP collector to receive a log");let K=X.requests[0];z(K?.path==="/v1/logs",`Expected OTLP logs path, got ${K?.path??"no request"}`);let Y=String(K?.headers["content-type"]??"");z(Y.includes("application/x-protobuf"),`Expected protobuf content type, got ${Y||"none"}`),z(K?.headers["x-ecv-api-key"]==="wire-secret","Expected configured exporter header"),z((K?.body.byteLength??0)>0,"Expected a non-empty OTLP protobuf body")}finally{await Q.otel?.shutdown?.(),Q.close(),await X.close()}}}}function O5(){return{tests:[{name:"otel module is inactive (not-ready) when RemoteConfig omits otel (§22.2)",fn:async()=>{let $=N5({id:"otel-off"}),Q=u1($,{validators:l1()});try{await Q.ready(500),z(Q.otel?.isReady?.()!==!0,"Expected ion.otel to be not-ready with no RemoteConfig.otel");let K=Q.readiness();w(K.modules.otel.active,!1,"Expected modules.otel.active === false when unconfigured");let Y=Q.requireReady(["otel"]);z(Y.ok===!1,'Expected requireReady(["otel"]) to fail when otel is unconfigured'),z(Y.notReady.some((Z)=>Z.name==="otel"),"Expected otel to be listed among the not-ready dependencies")}finally{Q.close?.()}}},$Q(),{name:"RemoteConfig.otel activates the module; de-configuration re-arms readiness (§22.2)",fn:async()=>{let $=N5({id:"otel-rearm",otel:{...y2}}),Q=u1($,{validators:l1()});try{await Q.ready(1000),z(Q.otel?.isReady?.()===!0,"Expected ion.otel.isReady() true after RemoteConfig.otel applied");let K=Q.readiness();w(K.modules.otel.active,!0,"Expected modules.otel.active === true once configured"),w(K.modules.otel.ready,!0,"Expected modules.otel.ready === true once started"),z(Q.requireReady(["otel"]).ok===!0,'Expected requireReady(["otel"]) to pass when active+ready'),$.push({id:"otel-rearm"}),await D1(()=>Q.otel?.isReady?.()!==!0),z(Q.otel?.isReady?.()!==!0,"Expected ion.otel.isReady() false after de-configuration");let Y=Q.readiness();w(Y.modules.otel.active,!1,"Expected modules.otel.active === false after de-config"),z(Q.requireReady(["otel"]).ok===!1,'Expected requireReady(["otel"]) to fail once otel is de-configured');let Z=!1;try{await Q.otel?.ready?.(50)}catch{Z=!0}z(Z,"Expected a re-armed ion.otel.ready(50) to reject while de-configured");let G={resolved:!1},W=Q.otel?.ready?.().then(()=>{G.resolved=!0}).catch(()=>{});await b2(60);let H=G.resolved===!1;z(H,"Expected a fresh no-timeout ready() to stay pending while inactive"),$.push({id:"otel-rearm",otel:{...y2}}),await D1(()=>Q.otel?.isReady?.()===!0),await W;let J=G.resolved===!0;z(J,"Expected the re-armed ready() waiter to resolve after re-activation"),z(Q.otel?.isReady?.()===!0,"Expected ion.otel.isReady() true after re-activation"),z(Q.requireReady(["otel"]).ok===!0,'Expected requireReady(["otel"]) to pass again after re-activation')}finally{Q.close?.()}}},{name:"otel rebuilds exporters when its referenced KMS key rotates",fn:async()=>{let $=e8({kind:"kms",slots:["k1"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()}]}),Q=u1($,{validators:l1()});try{await Q.ready(1000),z(await D1(()=>Q.otel?.isReady?.()===!0),"Expected OTel to start with KMS token");let K=0,Y=Q.otel?.afterChange?.(()=>{K+=1});$.pushPool({kind:"kms",slots:["k2"],keys:[{id:"k1",state:"deprecated",value:"token-1",notAfter:new Date(Date.now()+60000).toISOString()},{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),z(await D1(()=>K>0),"Expected KMS active-key change to rebuild managed OTel"),z(await D1(()=>Q.otel?.isReady?.()===!0),"Expected OTel ready after credential rebuild"),w(K,1,"Expected the active-key rotation to rebuild managed OTel exactly once");let Z=Q.kms?.getPool("otel-ecv");z(Z?.accepts("token-1")===!0,"Expected OTel KMS overlap to accept the deprecated key"),z(Z.accepts("token-2"),"Expected OTel KMS overlap to accept the new active key"),w(await Z.value(),"token-2","Expected OTel KMS value() to expose the rotated active key"),$.pushPool({kind:"kms",slots:["k2"],keys:[{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),z(!Z.accepts("token-1"),"Expected overlap removal to stop accepting the deprecated key"),await b2(80),w(K,1,"Expected overlap-only cleanup not to rebuild OTel when the active key is unchanged"),z(Q.otel?.isReady?.()===!0,"Expected OTel to remain ready after overlap-only cleanup"),Y?.()}finally{Q.close()}}},{name:"an invalid RemoteConfig.otel provider config fails the config apply (§22.2)",fn:async()=>{let $=N5({id:"otel-invalid",otel:{type:"datadog",name:"ecv-otel",traces:!1,metrics:!1,logs:!1}}),Q=u1($,{validators:l1()});try{let K=!1;try{await Q.ready(500)}catch{K=!0}z(K,"Expected an invalid otel provider config to reject readiness at apply"),z(Q.configStatus?.lastError!==void 0,"Expected configStatus.lastError to record the failed otel apply")}finally{Q.close?.()}}}]}}async function QQ(){let X=performance.now(),{tests:$}=O5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"OpenTelemetry",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{ionite as K1}from"@ionite/server";import{createValidators as Y1}from"@ionite/valibot";var Z1="/api/ionite",X1="http://localhost";function B1(X){return{async load(){return X},async subscribe(){return},secret:{}}}async function _1(X){let $=await X.text();try{return JSON.parse($)}catch{throw Error(`Expected a JSON body, got: ${$.slice(0,120)}`)}}function M5(){return{tests:[{name:"POST {basePath}/readyz returns a structured 405 with an Allow header (§11.2)",fn:async()=>{let X=K1(B1({id:"protocol-readyz-405"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/readyz`,{method:"POST"}));w($.status,405,"POST to readyz must be 405 Method Not Allowed"),z($.headers.get("Allow")?.includes("GET")??!1,"Allow header must advertise GET");let Q=await _1($);w(Q.error,"method_not_allowed","body carries the stable method_not_allowed code"),X.close?.()}},{name:"GET {basePath}/readyz is reachable and reports config applied",fn:async()=>{let X=K1(B1({id:"protocol-readyz-get"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/readyz`));z($.status===200||$.status===503,"readyz GET returns a readiness status");let Q=await _1($);w(Q.applied,!0,"readyz reports config applied after ion.ready()"),X.close?.()}},{name:"POST {basePath}/capabilitiez returns 405 before authentication (§11.2)",fn:async()=>{let X=K1(B1({id:"protocol-cap-405",capabilitiez:!0}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/capabilitiez`,{method:"POST"}));w($.status,405,"POST to capabilitiez must be 405");let Q=await _1($);w(Q.error,"method_not_allowed","capabilitiez 405 carries the stable code"),X.close?.()}},{name:"GET {basePath}/capabilitiez returns JSON 404 when disabled (§11.2)",fn:async()=>{let X=K1(B1({id:"protocol-cap-disabled"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/capabilitiez`));w($.status,404,"disabled capabilitiez is 404");let Q=await _1($);w(Q.error,"not_found","disabled capabilitiez returns the stable JSON not_found code"),X.close?.()}},{name:"GET {basePath}/capabilitiez returns 503 before config is applied",fn:async()=>{let X,Q=K1({load:()=>new Promise((Z)=>{X=Z}),async subscribe(){return},secret:{}},{validators:Y1()}),K=await Q.handler(new Request(`${X1}${Z1}/capabilitiez`));w(K.status,503,"capabilitiez must not expose a manifest before config is applied");let Y=await _1(K);w(Y.error,"ionite is not ready","not-ready capabilitiez returns the stable error code"),X?.({id:"protocol-cap-not-ready",capabilitiez:!0}),await Q.ready(1000),Q.close?.()}},{name:"GET {basePath}/capabilitiez requires workload authentication when enabled",fn:async()=>{let X=K1(B1({id:"protocol-cap-auth",capabilitiez:!0}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/capabilitiez`));w($.status,401,"enabled capabilitiez must reject an unauthenticated caller"),X.close?.()}},{name:"an unknown ionite route returns JSON 404 { error: not_found } (§9.2)",fn:async()=>{let X=K1(B1({id:"protocol-unknown-404"}),{validators:Y1()});await X.ready(1000);let $=await X.handler(new Request(`${X1}${Z1}/does-not-exist`));w($.status,404,"unknown route is 404");let Q=await _1($);w(Q.error,"not_found","unknown route returns the stable JSON not_found code"),X.close?.()}},{name:"a throwing handler is caught and sanitized to 500 { error: internal_error } (§9.2)",fn:async()=>{let $=K1(B1({id:"protocol-throw-500"}),{validators:Y1(),routing:{resolve:()=>({module:"sso",kind:"sso.handler",handler:async()=>{throw Error("LEAKED_INTERNAL_DETAIL_42")}})}});await $.ready(1000);let Q=await $.handler(new Request(`${X1}${Z1}/anything`));w(Q.status,500,"an unhandled handler throw becomes a 500");let K=await Q.text();z(!K.includes("LEAKED_INTERNAL_DETAIL_42"),"the raw error message must not leak to the caller");let Y=JSON.parse(K);w(Y.error,"internal_error","a thrown error collapses to the stable internal_error code"),$.close?.()}}]}}async function KQ(){let{tests:X}=M5(),$=[];for(let Q of X)try{await Q.fn(),$.push({name:Q.name,passed:!0})}catch(K){$.push({name:Q.name,passed:!1,error:K instanceof Error?K.message:String(K)})}return $}import{defineSecretType as YQ,SecretContentTypeMismatchError as ZQ,waitOn as XQ}from"@ionite/core";import{generateLfvEd25519KeyPair as E2,signLfvPayload as P9,stringifyLfvJwks as h2}from"@ionite/core/server";import{lfvVault as C2,subscribeSecretPathWithHttpPolling as zQ,vault as w9}from"@ionite/server";var I0={lfvServerUrl:"http://localhost:3599/api/lfv",clientId:"ecv-hardening-client",path:"/ecv/hardening/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"},o0=(X)=>new Promise(($)=>setTimeout($,X));async function D9(X,$,Q,K){let Y=String($.request_id),Z="issued_at"in $?$:{...$,issued_at:Date.now()},G=JSON.stringify(Z),W=await P9({privateKeyJwk:Q,requestId:Y,rawBody:G});return new Request(`http://localhost${X}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":Y,"X-LFV-Key-Id":K,"X-LFV-Signature":W},body:G})}async function GQ(X,$,Q){let K=String(X.request_id),Y=JSON.stringify(X),Z=await P9({privateKeyJwk:$,requestId:K,rawBody:Y});return new Response(Y,{status:200,headers:{"Content-Type":"application/json","X-LFV-Request-Id":K,"X-LFV-Key-Id":Q,"X-LFV-Signature":Z}})}function B9(X){if(typeof X==="string")return X;if(X instanceof URL)return X.toString();return X.url}function WQ(X,$=5){let Q=[],K=X;for(let Y=0;Y<$&&K!=null;Y+=1)if(K instanceof Error)Q.push(K.message),K=K.cause;else{Q.push(String(K));break}return Q.join(" <- ")}function HQ(X,$){let Q=X?.headers;if(!Q)return null;if(Q instanceof Headers)return Q.get($);if(Array.isArray(Q)){let Y=Q.find(([Z])=>Z.toLowerCase()===$.toLowerCase());return Y?Y[1]:null}let K=Q;for(let[Y,Z]of Object.entries(K))if(Y.toLowerCase()===$.toLowerCase())return Z;return null}function T5(){return{tests:[{name:"upstream error bodies are redacted from thrown errors (no secret leaks into configStatus.lastError)",fn:async()=>{let Q=w9({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),K=globalThis.fetch;globalThis.fetch=async()=>{return new Response(JSON.stringify({errors:["SECRET-DO-NOT-LEAK-7f3a91"],leaked:"SECRET-DO-NOT-LEAK-7f3a91"}),{status:500,statusText:"Internal Server Error"})};try{let Y=!1,Z="";try{await Q.getSecret("app/config")}catch(G){Y=!0,Z=WQ(G)}z(Y,"a non-200 vault read must throw (fail closed), not return stale/empty data"),z(!Z.includes("SECRET-DO-NOT-LEAK-7f3a91"),`thrown error must not include the upstream body (got: ${Z})`),z(Z.includes("500"),`redacted error should still carry the structured status (got: ${Z})`)}finally{globalThis.fetch=K,Q.close?.()}}},{name:"http polling never overlaps and surfaces degraded/recovered health",fn:async()=>{let $=0,Q=0,K=0,Y=!1,Z=[],W=await zQ({getFullSecret:async(J)=>{if($+=1,Q=Math.max(Q,$),K+=1,await o0(25),$-=1,Y)throw Error("simulated upstream failure");return{data:{},metadata:{path:J,version:1,created:new Date}}}},"/p",()=>{},15,void 0,void 0,{onHealth:(J)=>Z.push(J)});await o0(180),w(Q,1,"HTTP polls must never overlap (self-scheduling chain)"),Y=!0,await o0(120),z(Z.includes(!1),"a failed poll must surface unhealthy (degraded isAvailable) instead of hammering"),Y=!1,await o0(120),w(Z[Z.length-1],!0,"recovery must surface healthy again"),W();let H=K;await o0(120),w(K,H,"no polls may run after unsubscribe (loop halts on teardown)")}},{name:"secret handles lazily decode typed env, text, binary, and legacy JSONC values",fn:async()=>{let $=w9({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),Q=globalThis.fetch;globalThis.fetch=async(K)=>{let Y=new URL(B9(K)).pathname.split("/").pop(),Z={path:`/${Y}`,version:1,created_time:new Date(0).toISOString()};if(Y==="env")return Response.json({data:{data:{API_KEY:"secret",PORT:"443"},metadata:{...Z,content_type:{format:"env",mime_type:"text/x-dotenv"}}}});if(Y==="text")return Response.json({data:{data:{content:"hello"},metadata:{...Z,content_type:{format:"text",mime_type:"text/plain"}}}});if(Y==="binary")return Response.json({data:{data:{content_base64:"AAEC/w=="},metadata:{...Z,content_type:{format:"binary",mime_type:"application/octet-stream"}}}});return Response.json({data:{data:{legacy:!0},metadata:Z}})};try{let K=await $.get("env").env();w(K.API_KEY,"secret","env() must decode the flat key/value object"),w(K.PORT,"443","env() must preserve string values"),w(await $.get("text").text(),"hello","text() must unwrap the content field");let Y=await $.get("binary").bytes();w(Array.from(Y).join(","),"0,1,2,255","bytes() must fully decode base64");let Z=await $.get("legacy").json();w(Z.legacy,!0,"legacy secrets without content_type must default to JSONC");let G=YQ({id:"expected-json",format:"jsonc"}),W;try{await $.get("text").parse(G)}catch(J){W=J}z(W instanceof ZQ,"parse() must throw the structured mismatch error before decoding");let H=W;w(H.expected.mimeType,"application/json","mismatch must expose expected MIME type"),w(H.received.mimeType,"text/plain","mismatch must expose received MIME type")}finally{globalThis.fetch=Q,$.close?.()}}},{name:"LFV OTP-as-possession-proof: no client secret, OTP arrives only via signed callback",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await E2("ecv-otp-kid"),K=C2({...I0,verifyJwks:h2(Q)}),Y=0,Z=0,G=!1,W=7,H=globalThis.fetch;globalThis.fetch=async(J,j)=>{let D=B9(J);if(HQ(j,"X-LFV-Signature")!==null)G=!0;let P=JSON.parse(String(j?.body??"{}"));if(D.endsWith("/otp"))return Y+=1,await K.handleDelivery(await D9(I0.deliveryEndpoint,{request_id:P.request_id,type:"otp",otp:"otp-value",path:I0.path,action:P.action},$,"ecv-otp-kid")),new Response(JSON.stringify({request_id:P.request_id,status:"pending"}),{status:202});if(D.endsWith("/secrets/request"))return Z+=1,GQ({request_id:P.request_id,status:"completed",path:I0.path,data:{data:{marker:"possession-ok"},metadata:{path:I0.path,version:W,content_type:{format:"jsonc",mime_type:"application/vnd.ionite.ecv+json",schema_url:"https://example.com/ecv.schema.json",type_id:"ecv-payload"}}}},$,"ecv-otp-kid");return new Response("{}",{status:200})};try{let J=await Promise.all(Array.from({length:5},()=>K.getSecret(I0.path)));for(let D of J)w(D.marker,"possession-ok","each concurrent read should resolve from the one cycle");let j=await K.getFullSecret(I0.path);w(j.metadata.contentType?.format,"jsonc","LFV delivery must preserve the secret format"),w(j.metadata.contentType?.mimeType,"application/vnd.ionite.ecv+json","LFV delivery must preserve the MIME type"),w(j.metadata.contentType?.schemaUrl,"https://example.com/ecv.schema.json","LFV delivery must preserve the schema URL"),w(j.metadata.contentType?.typeId,"ecv-payload","LFV delivery must preserve the type id"),w(Y,1,"concurrent reads must issue exactly one OTP request (single-flight)"),w(Z,1,"concurrent reads must issue exactly one secret request (single-flight)"),z(!G,"client must not send X-LFV-Signature (no client-held secret)")}finally{globalThis.fetch=H,K.close?.()}}},{name:"self-redeeming LFV event storms single-flight OTP issuance per path",fn:async()=>{let{privateJwk:$,publicJwks:Q}=await E2("ecv-event-flight-kid"),K=C2({...I0,verifyJwks:h2(Q),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),Y=globalThis.fetch,Z=0,G,W=new Promise((H)=>{G=H});globalThis.fetch=async()=>{return Z+=1,W};try{for(let H=0;H<5;H+=1){let J=await K.handleEvents(await D9(I0.eventsEndpoint,{request_id:crypto.randomUUID(),path:I0.path,event:"updated"},$,"ecv-event-flight-kid"));w(J.status,200,"event callback is accepted")}await o0(20),w(Z,1,"concurrent event refreshes issue one OTP request for the path"),G?.(Response.json({status:"pending"},{status:202})),await o0(20)}finally{G?.(Response.json({status:"pending"},{status:202})),globalThis.fetch=Y,K.close?.()}}},{name:"LFV close() mid-outage settles pending waiters instead of leaking them",fn:async()=>{let{publicJwks:$}=await E2("ecv-close-kid"),Q=C2({...I0,verifyJwks:h2($)}),K=globalThis.fetch;globalThis.fetch=async(Y,Z)=>{let G=JSON.parse(String(Z?.body??"{}"));return new Response(JSON.stringify({request_id:G.request_id,status:"pending"}),{status:202})};try{let Y=Q.getSecret(I0.path),Z=!1;Y.catch(()=>{Z=!0}),await o0(40),Q.close?.();let G=!1,W="";try{await Y}catch(H){G=!0,W=H instanceof Error?H.message:String(H)}z(G,"close() must settle the pending read (reject), not leak the waiter"),z(W.toLowerCase().includes("closed"),`rejection should explain the source closed (got: ${W})`),z(Z,"pending waiter promise must settle after close()")}finally{globalThis.fetch=K}}},{name:"waitOn probes never overlap and stop after success (timers cleared)",fn:async()=>{let $=0,Q=0,K=0,Y=!1,Z=globalThis.fetch;globalThis.fetch=async()=>{return $+=1,Q+=1,K=Math.max(K,Q),await o0(40),Q-=1,new Response("{}",{status:200})};let G=setTimeout(()=>{Y=!0},150);try{await XQ("http://localhost/ecv/health",()=>Y,10,0,5000),w(K,1,"waitOn must never run overlapping probes (self-scheduling chain)"),z($>=2,"the probe chain should have iterated at least twice before becoming ready");let W=$;await o0(120),w($,W,"no probe may fire after waitOn resolves (retry timer cleared)")}finally{clearTimeout(G),globalThis.fetch=Z}}}]}}async function JQ(){let X=performance.now(),{tests:$}=T5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Secrets hardening",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}var L9={loginPath:"/api/auth/login",callbackPath:"/api/auth/callback",userPath:"/api/auth/user",logoutPath:"/api/auth/logout",backChannelLogoutPath:"/api/auth/logout/backchannel",tokenPath:"/api/auth/token",refreshPath:"/api/auth/refresh"};async function V9(X,$){return N("SSO Login Endpoint",async()=>{let Q=X.loginPath??"/api/auth/login",K=await $(Q);z(K.status===302,`Expected 302 redirect, got ${K.status}`);let Y=K.headers.get("Location");if(z(Y!==null,"Missing Location header in redirect"),X.expectedAuthorizationUrlPattern)z(X.expectedAuthorizationUrlPattern.test(Y),`Authorization URL does not match expected pattern: ${Y}`);let Z=new URL(Y);z(Z.searchParams.has("client_id"),"Missing client_id in authorization URL"),z(Z.searchParams.has("redirect_uri"),"Missing redirect_uri in authorization URL"),z(Z.searchParams.has("response_type"),"Missing response_type in authorization URL"),z(Z.searchParams.has("scope"),"Missing scope in authorization URL"),z(Z.searchParams.has("state"),"Missing state in authorization URL"),z(Z.searchParams.has("code_challenge"),"Missing code_challenge (PKCE) in authorization URL"),z(Z.searchParams.has("code_challenge_method"),"Missing code_challenge_method (PKCE) in authorization URL");let G=y0(K.headers),W=Array.from(G.keys()).some((H)=>H.includes(".state"));return z(W,"Missing state cookie in response"),{details:{authorizationUrl:Y,pkceEnabled:!0,stateParameter:Z.searchParams.get("state")}}})}async function N9(X,$){return N("SSO User Endpoint (Unauthenticated)",async()=>{let Q=X.userPath??"/api/auth/user",K=await $(Q);return z(K.status===401,`Expected 401 Unauthorized, got ${K.status}`),{details:{status:K.status}}})}async function O9(X,$){return N("SSO Logout Endpoint",async()=>{let Q=X.logoutPath??"/api/auth/logout",K=await $(`${Q}?redirect=/`);z(K.status===302||K.status===200,`Expected 302 or 200, got ${K.status}`);let Y=y0(K.headers),Z=[];for(let[H,J]of Y.entries())if(J===""||J.includes("Max-Age=0"))Z.push(H);let G=K.headers.getSetCookie?.()??[],W=G.some((H)=>H.includes("Max-Age=0")||H.includes("expires=Thu, 01 Jan 1970"));return z(W||Z.length>0||G.length===0,"Logout should clear session cookies"),{details:{status:K.status,clearedCookies:Z}}})}async function M9(X,$){return N("SSO Back-Channel Logout Endpoint",async()=>{let Q=X.backChannelLogoutPath??"/api/auth/logout/backchannel",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:""});return z(K.status===400||K.status===200||K.status===404,`Expected 400 or 200, got ${K.status}`),{details:{status:K.status,endpointExists:K.status!==404}}})}async function T9(X,$){return N("SSO Callback Endpoint (Invalid)",async()=>{let Q=X.callbackPath??"/api/auth/callback",K=await $(`${Q}?code=invalid&state=invalid`);z(K.status===302,`Expected redirect for invalid callback, got ${K.status}`);let Y=K.headers.get("Location")??"";if(!Y.startsWith("/")){let Z=new URL(`${T0(X)}/`).origin;z(new URL(Y).origin===Z,`Failure redirect must stay same-origin: ${Y}`)}return{details:{status:K.status,location:Y,handlesInvalidCallback:!0}}})}async function jQ(X,$,Q){return N("SSO Callback Error Surfaced",async()=>{let K=X.callbackPath??"/api/auth/callback",Y=await $(`${K}?code=invalid&state=invalid`);z(Y.status===302,`Expected redirect for invalid callback, got ${Y.status}`);let Z=Y.headers.get("Location")??"";return z(Q.test(Z),`Failure redirect does not surface an error code (expected ${Q}): ${Z}`),{details:{status:Y.status,location:Z}}})}async function x9(X,$){if(!X.tokenPath)return W1("SSO Token Endpoint (Unauthenticated)","Token path not configured");let Q=X.tokenPath;return N("SSO Token Endpoint (Unauthenticated)",async()=>{let K=await $(Q);return z(K.status===401||K.status===404,`Expected 401 Unauthorized or 404, got ${K.status}`),{details:{status:K.status}}})}async function _9(X,$){if(!X.refreshPath)return W1("SSO Refresh Endpoint (Unauthenticated)","Refresh path not configured");let Q=X.refreshPath;return N("SSO Refresh Endpoint (Unauthenticated)",async()=>{let K=await $(Q);return z(K.status===401||K.status===404,`Expected 401 Unauthorized or 404, got ${K.status}`),{details:{status:K.status}}})}function q9(X){return Array.from(X.keys()).filter(($)=>$.includes(".state"))}function F9(X){return X.endsWith(".access")||X.endsWith(".id")||X.endsWith(".refresh")||X.endsWith(".control")}function wQ(X){for(let $ of X.keys()){let Q=$.indexOf(".state");if(Q>0)return $.slice(0,Q);if($.endsWith(".session"))return $.slice(0,-8)}return}function DQ(X,$){return(X.getSetCookie?.()??[]).some((K)=>{let Y=K.split("=")[0]?.trim()??"";if(!$(Y))return!1;return K.includes("Max-Age=0")||/expires=Thu, 01 Jan 1970/i.test(K)})}async function A2(X,$,Q={}){let K=X.loginPath;if(!K)throw Error("Login path not configured");let Y=Q.redirectParam?`?redirect=${encodeURIComponent(Q.redirectParam)}`:"",Z=await $(`${K}${Y}`);if(Z.status!==302)throw Error(`Login did not redirect: ${Z.status}`);let G=Z.headers.get("Location");if(!G)throw Error("No redirect location from login");let W=y0(Z.headers),H=new URL(G).searchParams.get("state");if(Q.bogusCode){let B=new URL(G).searchParams.get("redirect_uri");if(!B||!H)throw Error("Missing redirect_uri/state on authorization URL");let q=new URL(B).pathname;return{callback:await $(`${q}?code=bogus-code&state=${encodeURIComponent(H)}`,{headers:{Cookie:f0(W)}}),state:H}}let J=await globalThis.fetch(G,{redirect:"manual"});if(J.status!==302)throw Error(`Auth did not redirect: ${J.status}`);let j=J.headers.get("Location");if(!j)throw Error("No redirect location from auth");let D=new URL(j).pathname+new URL(j).search;return{callback:await $(D,{headers:{Cookie:f0(W)}}),state:H}}async function R9(X,$){return N("SSO State Cookie Multi-Tab Isolation",async()=>{let Q=X.loginPath??"/api/auth/login",[K,Y]=await Promise.all([$(Q),$(Q)]);z(K.status===302&&Y.status===302,"Both login attempts should redirect");let Z=new URL(K.headers.get("Location")??"").searchParams.get("state"),G=new URL(Y.headers.get("Location")??"").searchParams.get("state");z(!!Z&&!!G&&Z!==G,"Each login attempt must mint a distinct state");let W=q9(y0(K.headers)),H=q9(y0(Y.headers));z(W.length===1&&H.length===1,"Each login should set exactly one state cookie"),z(W[0]!==H[0],"Concurrent logins must use isolated (distinct-name) state cookies"),z(W[0].includes(Z)&&H[0].includes(G),"The state value must be encoded in the state cookie name");let J=(K.headers.getSetCookie?.()??[]).find((j)=>j.split("=")[0]?.includes(".state"));return z(!!J,"Login must emit a Set-Cookie for the state cookie"),z(/SameSite=Lax/i.test(J),`State cookie must be SameSite=Lax for the cross-site IdP callback, got: ${J}`),{details:{state1:Z,state2:G,cookie1:W[0],cookie2:H[0],stateSetCookie:J}}})}async function v9(X,$){return N("SSO State Cookie Cleared On Error",async()=>{let Q;try{Q=await A2(X,$,{bogusCode:!0})}catch(Z){return{details:{skipped:!0,reason:Z instanceof Error?Z.message:String(Z)}}}let{callback:K,state:Y}=Q;return z(K.status===302,`Failed callback should redirect, got ${K.status}`),z(DQ(K.headers,(Z)=>Z.includes(".state")&&(Y?Z.includes(Y):!0)),"Callback error path must clear the per-attempt state cookie"),{details:{status:K.status,state:Y}}})}async function U9(X,$){return N("SSO Open-Redirect Denied (same-origin floor)",async()=>{let Q;try{Q=await A2(X,$,{redirectParam:"https://evil.example/pwned"})}catch(Z){return{details:{skipped:!0,reason:Z instanceof Error?Z.message:String(Z)}}}let{callback:K}=Q;z(K.status===302,`Successful callback should redirect, got ${K.status}`);let Y=K.headers.get("Location")??"";if(z(!Y.includes("evil.example"),`Open redirect leaked to external origin: ${Y}`),!Y.startsWith("/")){let Z=new URL(`${T0(X)}/`).origin;z(new URL(Y).origin===Z,`Post-login redirect must stay same-origin: ${Y}`)}return{details:{location:Y}}})}async function b9(X,$){return N("SSO Opaque Session Cookie Only",async()=>{let{cookies:Q,success:K,error:Y}=await d1(X,$);z(K,Y||"Login flow failed");let Z=Array.from(Q.keys());z(Z.some((j)=>j.includes(".session")),"Opaque login must set a `.session` cookie");let G=Z.find(F9);z(!G,`Opaque-only login must not set token cookies, found: ${G}`);let W=X.userPath;if(!W)throw Error("User path not configured");let H=await $(W,{headers:{Cookie:f0(Q)}});z(H.status===200,`User endpoint returned ${H.status} after login, expected 200`);let J=Array.from(y0(H.headers).keys()).find(F9);return z(!J,`Session resolution must not set token cookies, found: ${J}`),{details:{sessionCookieOnly:!0}}})}async function y9(X,$){return N("SSO Legacy Token Cookies Rejected",async()=>{let Q=X.loginPath??"/api/auth/login",K=X.userPath??"/api/auth/user",Y=await $(Q);z(Y.status===302,`Login should redirect, got ${Y.status}`);let Z=wQ(y0(Y.headers));if(!Z)return{details:{skipped:!0,reason:"Could not derive cookie prefix from login response"}};let G=new Map([[`${Z}.access`,"legacy-access"],[`${Z}.id`,"legacy-id"],[`${Z}.refresh`,"legacy-refresh"],[`${Z}.control`,btoa(JSON.stringify({token_type:"Bearer",expires_in:3600}))]]),W=await $(K,{headers:{Cookie:f0(G)}});return z(W.status===401,`Legacy token cookies must not authenticate (expected 401), got ${W.status}`),{details:{status:W.status,prefix:Z}}})}async function d1(X,$){let Q=X.loginPath;if(!Q)return{cookies:new Map,success:!1,error:"Login path not configured"};try{let K=await $(Q);if(K.status!==302)return{cookies:new Map,success:!1,error:`Login did not redirect: ${K.status}`};let Y=K.headers.get("Location");if(!Y)return{cookies:new Map,success:!1,error:"No redirect location from login"};let Z=y0(K.headers),G=await globalThis.fetch(Y,{redirect:"manual"});if(G.status!==302)return{cookies:new Map,success:!1,error:`Auth did not redirect: ${G.status}`};let W=G.headers.get("Location");if(!W)return{cookies:new Map,success:!1,error:"No redirect location from auth"};let H=new URL(W).pathname+new URL(W).search,J=await $(H,{headers:{Cookie:f0(Z)}});if(J.status!==302)return{cookies:new Map,success:!1,error:`Callback did not redirect: ${J.status}`};return{cookies:y0(J.headers),success:!0}}catch(K){return{cookies:new Map,success:!1,error:K instanceof Error?K.message:String(K)}}}async function E9(X,$){return N("SSO JIT User Provisioning Flow",async()=>{let{cookies:Q,success:K,error:Y}=await d1(X,$);z(K,Y||"Login flow failed");let Z=Array.from(Q.keys()).some((J)=>J.includes(".session"));z(Z,"No opaque session cookie set after login");let G=X.userPath;if(!G)throw Error("User path not configured");let W=await $(G,{headers:{Cookie:f0(Q)}});z(W.status===200,`User endpoint returned ${W.status} after login, expected 200`);let H=await W.json();return z(H.id!==void 0,"User data missing id field"),BQ(X,H),{details:{userId:H.id,userName:H.userName,name:H.name,email:H.email,avatar:H.avatar,jitFlowCompleted:!0}}})}function BQ(X,$){let Q=X.expectedUser;if(!Q)return;if(Q.id!==void 0)z($.id===Q.id,`Expected user id ${Q.id}, got ${String($.id)}`);if(Q.userName!==void 0)z($.userName===Q.userName,`Expected username ${Q.userName}, got ${String($.userName)}`);if(Q.name!==void 0)z($.name===Q.name,`Expected name ${Q.name}, got ${String($.name)}`);if(Q.email!==void 0)z($.email===Q.email,`Expected email ${Q.email}, got ${String($.email)}`);if(Q.avatar!==void 0)z($.avatar===Q.avatar,`Expected avatar ${Q.avatar}, got ${String($.avatar)}`);if(Q.requireSso)z($.sso!==void 0,"Expected SSO auth envelope to be preserved on user response")}async function PQ(X){let $=performance.now(),Q={...L9,...X},K=r(Q),Y=[];Y.push(await V9(Q,K)),Y.push(await N9(Q,K)),Y.push(await O9(Q,K)),Y.push(await M9(Q,K)),Y.push(await T9(Q,K)),Y.push(await x9(Q,K)),Y.push(await _9(Q,K)),Y.push(await R9(Q,K)),Y.push(await v9(Q,K)),Y.push(await U9(Q,K)),Y.push(await y9(Q,K)),Y.push(await E9(Q,K)),Y.push(await b9(Q,K));let Z=performance.now()-$,G=Y.filter((J)=>J.passed).length,W=Y.filter((J)=>!J.passed).length,H=Y.filter((J)=>J.details?.skipped).length;return{suite:"SSO",passed:W===0,tests:Y,duration:Z,summary:{total:Y.length,passed:G-H,failed:W,skipped:H}}}function x5(X){let $={...L9,...X},Q=r($);return{tests:[{name:"login endpoint redirects to authorization URL",fn:async()=>{let Z=await V9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"user endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await N9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"callback endpoint redirects on invalid requests",fn:async()=>{let Z=await T9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"token endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await x9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"refresh endpoint returns 401 when unauthenticated",fn:async()=>{let Z=await _9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"state cookie is isolated per login attempt (multi-tab)",fn:async()=>{let Z=await R9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"state cookie is cleared on callback error",fn:async()=>{let Z=await v9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"cross-origin post-login redirect is floored to same-origin",fn:async()=>{let Z=await U9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"legacy token cookies do not authenticate (opaque-only)",fn:async()=>{let Z=await y9($,Q);if(!Z.passed)throw Error(Z.error)}}],ext:{createJITTests:()=>[{name:"user provisioning flow completes successfully",fn:async()=>{let Z=await E9($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"login sets only an opaque session cookie (no token cookies)",fn:async()=>{let Z=await b9($,Q);if(!Z.passed)throw Error(Z.error)}}],createLogoutTests:()=>[{name:"logout endpoint clears cookies",fn:async()=>{let Z=await O9($,Q);if(!Z.passed)throw Error(Z.error)}}],createBackChannelLogoutTests:()=>[{name:"endpoint exists",fn:async()=>{let Z=await M9($,Q);if(!Z.passed)throw Error(Z.error)}}],createCallbackErrorSurfacedTests:(Z)=>[{name:"callback error surfaced",fn:async()=>{let G=await jQ($,Q,Z);if(!G.passed)throw Error(G.error)}}]}}}import{ION_TENANT_MANAGE as qQ}from"@ionite/core";import{InMemoryTenantStore as FQ,InMemoryWorkloadTokenStore as h9,ionite as C9,tenantManager as LQ,validateWorkloadToken as VQ}from"@ionite/server";import{createValidators as A9}from"@ionite/valibot";function S9(X){return{async load(){return X},async subscribe(){return},secret:{}}}function k9(){return{id:"sso",workload:{incoming:[{issuer:"http://idp.example/api/t/master",jwksUri:"http://idp.example/jwks",audience:["eci"]}]},authz:{trustedProvisioningClients:[{issuer:"http://idp.example/api/t/master",audience:["eci"],clientId:"eci",permissions:[qQ.id]}]}}}function _5(){return{tests:[{name:"SSO-shaped ionite TM rejects tenant create without a workload token",fn:async()=>{let $=A9(),Q=C9(S9(k9()),{validators:$,stores:{workloadTokenStore:new h9}});await Q.ready();let Y=await LQ(Q,{basePath:"/api/tenants",store:new FQ,validators:$}).handler(new Request("http://sso.example/api/tenants",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:"acme",companyId:"acme",companyName:"Acme",environmentType:"DEV"})}));z(Y.status===401,`expected 401 without workload auth, got ${Y.status}`),await Q.close()}},{name:"SSO-shaped client registration rejects a request with no workload token",fn:async()=>{let $=C9(S9(k9()),{validators:A9(),stores:{workloadTokenStore:new h9}});await $.ready();let Q=await VQ(new Request("http://sso.example/api/admin/tenants/acme/clients",{method:"POST"}),$);z(Q.valid===!1,"missing bearer must not validate"),await $.close()}}]}}async function NQ(){return _5()}import*as P1 from"node:crypto";import{clearJwksCache as U5,setJwksClock as I2,setJwksRetry as I9,verifyIdToken as K0,verifyLogoutToken as R5}from"@ionite/core/server";import{createValidators as OQ}from"@ionite/valibot";var l="https://idp.ecv.local/realms/ecv",f="ecv-oidc-client",MQ="http://schemas.openid.net/event/backchannel-logout";function t(X){let{publicKey:$,privateKey:Q}=P1.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function S2(X){return Buffer.from(X).toString("base64url")}function o(X){let $={alg:X.headerAlg??"RS256",typ:"JWT"};if(X.headerKid!==null)$.kid=X.headerKid??X.key.kid;let Q=`${S2(JSON.stringify($))}.${S2(JSON.stringify(X.claims))}`,K=P1.createSign("RSA-SHA256");return K.update(Q),`${Q}.${S2(K.sign(X.key.privateKey))}`}function $0(X={}){let $=Math.floor(Date.now()/1000);return{iss:l,aud:f,sub:"ecv-user",iat:$,exp:$+3600,...X}}async function J0(X,$,Q){let K=globalThis.fetch;U5(X),globalThis.fetch=async(Y,Z)=>{if((typeof Y==="string"?Y:Y.toString())===X)return new Response(JSON.stringify($),{status:200,headers:{"Content-Type":"application/json"}});return K(Y,Z)};try{return await Q()}finally{globalThis.fetch=K,U5(X)}}async function U0(X,$,Q){try{await X()}catch(K){let Y=K instanceof Error?K.message:String(K);if(!Y.includes($))throw Error(`${Q}: expected error containing "${$}", got "${Y}"`);return}throw Error(`${Q}: expected rejection, but the call resolved`)}function Q0(){return`https://jwks.ecv.local/${P1.randomUUID()}`}function k2(X={}){let $=Math.floor(Date.now()/1000);return{iss:l,aud:f,iat:$,jti:P1.randomUUID(),sid:"ecv-session-1",events:{[MQ]:{}},...X}}async function v5(X,$,Q){let K=globalThis.fetch;U5(X),I9({maxRetries:0}),globalThis.fetch=async(Y,Z)=>{if((typeof Y==="string"?Y:Y.toString())===X){let W=$();if(W==="fail")return new Response("unavailable",{status:503});return new Response(JSON.stringify(W),{status:200,headers:{"Content-Type":"application/json"}})}return K(Y,Z)};try{return await Q()}finally{globalThis.fetch=K,U5(X),I2(),I9()}}function b5(){return{tests:[{name:"valid ID token (correct iss/aud/exp/kid) resolves claims",fn:async()=>{let $=t("ecv-valid-kid"),Q=Q0(),K=o({key:$,claims:$0()}),Y=await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}));if(Y.sub!=="ecv-user")throw Error(`expected sub "ecv-user", got "${String(Y.sub)}"`)}},{name:"wrong aud and wrong azp throws audience mismatch",fn:async()=>{let $=t("ecv-aud-kid"),Q=Q0(),K=o({key:$,claims:$0({aud:"someone-else",azp:"someone-else"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"audience mismatch","wrong aud/azp"))}},{name:"matching azp only (client absent from aud) passes",fn:async()=>{let $=t("ecv-azp-kid"),Q=Q0(),K=o({key:$,claims:$0({aud:"api://resource-server",azp:f})}),Y=await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}));if(Y.azp!==f)throw Error(`expected azp "${f}", got "${String(Y.azp)}"`)}},{name:"wrong iss throws issuer mismatch",fn:async()=>{let $=t("ecv-iss-kid"),Q=Q0(),K=o({key:$,claims:$0({iss:"https://attacker.example/realms/evil"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"issuer mismatch","wrong iss"))}},{name:"expired exp throws (beyond clock skew)",fn:async()=>{let $=t("ecv-exp-kid"),Q=Q0(),K=Math.floor(Date.now()/1000)-3600,Y=o({key:$,claims:$0({exp:K})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:Y,jwksUri:Q,issuer:l,audience:f}),"expired","expired token"))}},{name:"missing kid in header throws",fn:async()=>{let $=t("ecv-nokid-kid"),Q=Q0(),K=o({key:$,headerKid:null,claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"missing kid","missing kid"))}},{name:"unknown header kid fails closed after JWKS refresh (no first-key fallback)",fn:async()=>{let $=t("ecv-realkid"),Q=Q0(),K=o({key:$,headerKid:"kid-not-in-jwks",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Public key not found","unknown kid"))}},{name:"JWKS rotation: a token signed with a newly published kid verifies after refresh (§1.10)",fn:async()=>{let $=t("kid-1"),Q=t("kid-2"),K=Q0(),Y=[$.jwk];await v5(K,()=>({keys:Y}),async()=>{if(await K0({idToken:o({key:$,claims:$0()}),jwksUri:K,issuer:l,audience:f}),Y=[$.jwk,Q.jwk],(await K0({idToken:o({key:Q,claims:$0()}),jwksUri:K,issuer:l,audience:f})).sub!=="ecv-user")throw Error("rotated-key token did not verify")})}},{name:"JWKS 503 on a cold cache fails closed (§1.29)",fn:async()=>{let $=t("kid-cold"),Q=Q0(),K=o({key:$,claims:$0()});await v5(Q,()=>"fail",()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Failed to fetch JWKS","cold JWKS 503"))}},{name:"JWKS TTL expiry triggers a refresh that picks up rotation (§1.10)",fn:async()=>{let $=t("kid-ttl-1"),Q=t("kid-ttl-2"),K=Q0(),Y=Date.UTC(2026,0,1);I2(()=>Y);let Z=[$.jwk];await v5(K,()=>({keys:Z}),async()=>{if(await K0({idToken:o({key:$,claims:$0()}),jwksUri:K,issuer:l,audience:f}),Z=[Q.jwk],Y+=660000,(await K0({idToken:o({key:Q,claims:$0()}),jwksUri:K,issuer:l,audience:f})).sub!=="ecv-user")throw Error("post-TTL rotated token did not verify")})}},{name:"JWKS warm refresh failure serves last-known-good keys (§1.10)",fn:async()=>{let $=t("kid-warm"),Q=Q0(),K=Date.UTC(2026,0,1);I2(()=>K);let Y="ok";await v5(Q,()=>Y==="fail"?"fail":{keys:[$.jwk]},async()=>{if(await K0({idToken:o({key:$,claims:$0()}),jwksUri:Q,issuer:l,audience:f}),K+=660000,Y="fail",(await K0({idToken:o({key:$,claims:$0()}),jwksUri:Q,issuer:l,audience:f})).sub!=="ecv-user")throw Error("last-known-good keys were not served on refresh failure")})}},{name:"logout_token with events + sid (no nonce) verifies (§1.6)",fn:async()=>{let $=t("kid-logout-ok"),Q=Q0(),K=o({key:$,claims:k2()}),Y=await J0(Q,{keys:[$.jwk]},()=>R5({logoutToken:K,jwksUri:Q,issuer:l,audience:f}));if(Y.sid!=="ecv-session-1")throw Error(`expected sid, got "${String(Y.sid)}"`)}},{name:"logout_token containing a nonce is rejected (§1.6)",fn:async()=>{let $=t("kid-logout-nonce"),Q=Q0(),K=o({key:$,claims:k2({nonce:"must-not-be-here"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>R5({logoutToken:K,jwksUri:Q,issuer:l,audience:f}),"must not contain a nonce","logout_token nonce"))}},{name:"plain ID token (no events) rejected as a logout_token (§1.6)",fn:async()=>{let $=t("kid-logout-idtoken"),Q=Q0(),K=o({key:$,claims:$0({sid:"ecv-session-1"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>R5({logoutToken:K,jwksUri:Q,issuer:l,audience:f}),"missing back-channel logout event","id token as logout_token"))}},{name:"logout_token missing both sid and sub is rejected (§1.6)",fn:async()=>{let $=t("kid-logout-nosub"),Q=Q0(),K=o({key:$,claims:k2({sid:void 0,sub:void 0})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>R5({logoutToken:K,jwksUri:Q,issuer:l,audience:f}),"missing sid and sub","logout_token no sid/sub"))}},{name:"nonce: matching expectedNonce at the callback exchange passes (§1.8)",fn:async()=>{let $=t("ecv-nonce-ok"),Q=Q0(),K=o({key:$,claims:$0({nonce:"ecv-nonce-123"})});if((await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f,expectedNonce:"ecv-nonce-123"}))).nonce!=="ecv-nonce-123")throw Error("expected the nonce claim to round-trip")}},{name:"nonce: a different nonce claim is rejected when expectedNonce is set (§1.8)",fn:async()=>{let $=t("ecv-nonce-bad"),Q=Q0(),K=o({key:$,claims:$0({nonce:"attacker-nonce"})});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f,expectedNonce:"ecv-nonce-123"}),"nonce mismatch","nonce mismatch"))}},{name:"nonce: a MISSING nonce claim is rejected when expectedNonce is set (§1.8)",fn:async()=>{let $=t("ecv-nonce-missing"),Q=Q0(),K=o({key:$,claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f,expectedNonce:"ecv-nonce-123"}),"nonce mismatch","missing nonce claim"))}},{name:"nonce: refresh-path verification (no expectedNonce) accepts a token without a nonce (§1.8)",fn:async()=>{let $=t("ecv-nonce-refresh"),Q=Q0(),K=o({key:$,claims:$0()});if((await J0(Q,{keys:[$.jwk]},()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}))).sub!=="ecv-user")throw Error("nonce-less refresh verification failed")}},{name:'alg pinning: header alg "none" is rejected before key resolution (§1.9)',fn:async()=>{let $=t("ecv-alg-none"),Q=Q0(),K=o({key:$,headerAlg:"none",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Unsupported JWT alg","alg none"))}},{name:"alg pinning: HMAC header alg (HS256) is rejected (§1.9)",fn:async()=>{let $=t("ecv-alg-hs"),Q=Q0(),K=o({key:$,headerAlg:"HS256",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"Unsupported JWT alg","alg HS256"))}},{name:"alg pinning: an RSA JWK cannot verify a token claiming ES256 (§1.9)",fn:async()=>{let $=t("ecv-alg-cross"),Q=Q0(),K=o({key:$,headerAlg:"ES256",claims:$0()});await J0(Q,{keys:[$.jwk]},()=>U0(()=>K0({idToken:K,jwksUri:Q,issuer:l,audience:f}),"does not match token alg","alg/JWK-type cross"))}},{name:"token response validator rejects a body missing access_token (§1.11 refresh fails closed)",fn:async()=>{let $=OQ();if(!(await $.sso.tokenResponse.validate({token_type:"Bearer"})).issues)throw Error("expected validation issues for a token response missing access_token");if((await $.sso.tokenResponse.validate({access_token:"a.b.c",id_token:"d.e.f",token_type:"Bearer"})).issues)throw Error("expected a minimal valid token response to pass validation")}}]}}async function TQ(){let X=performance.now(),{tests:$}=b5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"SSO ID token verification",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import*as z1 from"node:crypto";import{defaultLogger as xQ}from"@ionite/core";import{clearJwksCache as m9,sso as _Q}from"@ionite/core/server";import{InMemorySessionStore as RQ}from"@ionite/server";import{createValidators as vQ}from"@ionite/valibot";var f9="https://idp.ecv.local/realms/ecv",p9="ecv-oidc-client",g9="ion.sso",p2=86400,m2=7*p2,UQ=5000;function bQ(X){let{publicKey:$,privateKey:Q}=z1.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function f2(X){return Buffer.from(X).toString("base64url")}function yQ(X,$){let Q={alg:"RS256",typ:"JWT",kid:X.kid},K=`${f2(JSON.stringify(Q))}.${f2(JSON.stringify($))}`,Y=z1.createSign("RSA-SHA256");return Y.update(K),`${K}.${f2(Y.sign(X.privateKey))}`}function u9(X={}){let $=Math.floor(Date.now()/1000);return{iss:f9,aud:p9,sub:"ecv-user",iat:$,exp:$+3600,...X}}function h5(X,$){if(!X)throw Error($)}function EQ(X){return new Request("https://app.ecv.local/",{headers:{cookie:`${g9}.session=${X}`}})}function hQ(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"old-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"old-refresh",expires:new Date(Date.now()-1000).toISOString()},claims:u9({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}async function y5(X){let $=bQ(`kid-${z1.randomUUID()}`),Q=`https://jwks.ecv.local/${z1.randomUUID()}`,K=`https://idp.ecv.local/token/${z1.randomUUID()}`,Y=new RQ,Z=[],G={async create(H){Z.push(H.sid),await Y.create(H)},get:(H)=>Y.get(H),update:(H,J)=>Y.update(H,J),delete:(H)=>Y.delete(H)};await G.create(hQ("sid-window"));let W=globalThis.fetch;m9(Q),globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H.toString();if(j===Q)return new Response(JSON.stringify({keys:[$.jwk]}),{status:200,headers:{"Content-Type":"application/json"}});if(j===K){let D=yQ($,u9({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}));return new Response(JSON.stringify({access_token:"new-access",id_token:D,token_type:"Bearer",refresh_token:"new-refresh",expires_in:300,...X.refreshExpiresIn!==void 0?{refresh_expires_in:X.refreshExpiresIn}:{},scope:"openid"}),{status:200,headers:{"Content-Type":"application/json"}})}return W(H,J)};try{let H=vQ(),J=_Q(H.sso,xQ,void 0,{authority:f9,tokenUrl:K,authorizationUrl:"https://idp.ecv.local/authorize",clientId:p9,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Q,sessionStore:G,cookiesPrefix:g9,cookiesSecure:!1,sessionWindowSeconds:X.sessionWindowSeconds},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!J)throw Error("sso() returned undefined");let j=Date.now(),D=await J.resolveUser(EQ("sid-window"));h5(D.status==="authenticated",`refresh should succeed, got ${D.status}`);let P=Z.find((q)=>q!=="sid-window");h5(!!P,"refresh must rotate to a new sid");let B=await G.get(P);return h5(B!=null,"rotated session record must exist"),new Date(B.expiresAt).getTime()-j}finally{globalThis.fetch=W,m9(Q)}}function E5(X,$,Q){let K=$*1000;h5(Math.abs(X-K)<=UQ,`${Q}: expected a ~${$}s session window, got ${Math.round(X/1000)}s`)}function C5(){return{tests:[{name:"IdP refresh_expires_in always wins over the configured fallback",fn:async()=>{let $=await y5({sessionWindowSeconds:m2,refreshExpiresIn:3600});E5($,3600,"IdP-stated window must override sessionWindowSeconds")}},{name:"configured sessionWindowSeconds is honored when the IdP omits refresh_expires_in (no 24h cap)",fn:async()=>{let $=await y5({sessionWindowSeconds:m2});E5($,m2,"a window longer than the 24h default must be honored")}},{name:"defaults to 86400s when unset and the IdP omits refresh_expires_in",fn:async()=>{let $=await y5({});E5($,p2,"default fallback window")}},{name:"non-positive sessionWindowSeconds falls back to the 86400s default",fn:async()=>{let $=await y5({sessionWindowSeconds:0});E5($,p2,"a nonsense (<1) value must not produce an instant-death window")}}]}}async function CQ(){let X=performance.now(),{tests:$}=C5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"SSO session window",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import*as p0 from"node:crypto";import{defaultLogger as AQ}from"@ionite/core";import{clearJwksCache as l9,sso as SQ}from"@ionite/core/server";import{InMemorySessionStore as n9}from"@ionite/server";import{createValidators as kQ}from"@ionite/valibot";var i9="https://idp.ecv.local/realms/ecv",S5="ecv-oidc-client",A5="ecv-confidential-secret",o9="ion.sso";function IQ(X){let{publicKey:$,privateKey:Q}=p0.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function g2(X){return Buffer.from(X).toString("base64url")}function mQ(X,$){let Q={alg:"RS256",typ:"JWT",kid:X.kid},K=`${g2(JSON.stringify(Q))}.${g2(JSON.stringify($))}`,Y=p0.createSign("RSA-SHA256");return Y.update(K),`${K}.${g2(Y.sign(X.privateKey))}`}function u2(X={}){let $=Math.floor(Date.now()/1000);return{iss:i9,aud:S5,sub:"ecv-user",iat:$,exp:$+3600,...X}}function M0(X,$){if(!X)throw Error($)}function a9(X){return new Request("https://app.ecv.local/",{headers:{cookie:`${o9}.session=${X}`}})}function fQ(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"old-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"old-refresh",expires:new Date(Date.now()-1000).toISOString()},claims:u2({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}function pQ(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"live-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"live-refresh",expires:new Date(Date.now()+3600000).toISOString()},claims:u2({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}function t9(X,$){let Q=kQ(),K=SQ(Q.sso,AQ,void 0,{authority:i9,tokenUrl:$.tokenUrl,authorizationUrl:"https://idp.ecv.local/authorize",clientId:S5,clientSecret:$.clientSecret,revocationEndpoint:$.revocationEndpoint,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:$.jwksUri,sessionStore:X,cookiesPrefix:o9,cookiesSecure:!1},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!K)throw Error("sso() returned undefined");return K}async function d9(X){let $=IQ(`kid-${p0.randomUUID()}`),Q=`https://jwks.ecv.local/${p0.randomUUID()}`,K=`https://idp.ecv.local/token/${p0.randomUUID()}`,Y=new n9;await Y.create(fQ("sid-refresh"));let Z=globalThis.fetch;l9(Q);let G;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W.toString();if(J===Q)return new Response(JSON.stringify({keys:[$.jwk]}),{status:200,headers:{"Content-Type":"application/json"}});if(J===K){G=String(H?.body??"");let j=mQ($,u2({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}));return new Response(JSON.stringify({access_token:"new-access",id_token:j,token_type:"Bearer",refresh_token:"new-refresh",expires_in:3600,refresh_expires_in:86400,scope:"openid"}),{status:200,headers:{"Content-Type":"application/json"}})}return Z(W,H)};try{let H=await t9(Y,{tokenUrl:K,jwksUri:Q,clientSecret:X}).resolveUser(a9("sid-refresh"));return M0(H.status==="authenticated",`refresh should succeed, got ${H.status}`),M0(G!==void 0,"refresh must POST to the token endpoint"),new URLSearchParams(G)}finally{globalThis.fetch=Z,l9(Q)}}async function c9(X){let $=`https://jwks.ecv.local/${p0.randomUUID()}`,Q=`https://idp.ecv.local/token/${p0.randomUUID()}`,K=`https://idp.ecv.local/revoke/${p0.randomUUID()}`,Y=new n9;await Y.create(pQ("sid-logout"));let Z=globalThis.fetch,G;globalThis.fetch=async(W,H)=>{if((typeof W==="string"?W:W.toString())===K)return G=String(H?.body??""),new Response(null,{status:200});return Z(W,H)};try{return await t9(Y,{tokenUrl:Q,jwksUri:$,clientSecret:X,revocationEndpoint:K}).logout(a9("sid-logout")),M0(G!==void 0,"logout must POST the refresh token to the revocation endpoint"),new URLSearchParams(G)}finally{globalThis.fetch=Z}}function k5(){return{tests:[{name:"confidential client: refresh_token grant authenticates with client_secret (RFC 6749 §6)",fn:async()=>{let $=await d9(A5);M0($.get("grant_type")==="refresh_token","grant_type must be refresh_token"),M0($.get("refresh_token")==="old-refresh","refresh must send the stored refresh token"),M0($.get("client_id")===S5,"refresh must send client_id"),M0($.get("client_secret")===A5,"a confidential client must send client_secret on refresh — omitting it makes Keycloak reject with unauthorized_client at the first token expiry")}},{name:"public client: refresh_token grant sends no client_secret",fn:async()=>{let $=await d9(void 0);M0($.get("grant_type")==="refresh_token","grant_type must be refresh_token"),M0($.get("client_secret")===null,"a public (PKCE) client must not send a client_secret")}},{name:"confidential client: token revocation authenticates with client_secret (RFC 7009 §2.1)",fn:async()=>{let $=await c9(A5);M0($.get("token")==="live-refresh","revocation must send the stored refresh token"),M0($.get("token_type_hint")==="refresh_token","revocation must hint refresh_token"),M0($.get("client_id")===S5,"revocation must send client_id"),M0($.get("client_secret")===A5,"a confidential client must send client_secret on revocation — otherwise the refresh token silently outlives logout")}},{name:"public client: token revocation sends no client_secret",fn:async()=>{let $=await c9(void 0);M0($.get("token")==="live-refresh","revocation must send the stored refresh token"),M0($.get("client_secret")===null,"a public (PKCE) client must not send a client_secret")}}]}}async function gQ(){let X=performance.now(),{tests:$}=k5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"SSO token client auth",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{trimTrailingSlash as r9}from"@ionite/server";async function uQ(X){try{return await X.json()}catch{return{}}}async function c1(X){let $=r9(X.tenantPath??"/api/tenants"),Q=X.profile??"tenantManager",K=await k0(X.ecv.baseUrl,f1),Y={...X.tenant,configLocator:X.tenant.configLocator??X.ecv.configLocator,webhookUrl:X.tenant.webhookUrl??`${X.ecv.baseUrl}/webhook`},Z=await fetch(new URL($,X.baseUrl),{method:"POST",headers:{Authorization:`Bearer ${K}`,"Content-Type":"application/json"},body:JSON.stringify(Y),signal:AbortSignal.timeout(X.readiness?.requestTimeoutMs??15000)}),G=await uQ(Z);if(Z.status===409||G.status==="failed")throw Error(`Tenant ${Y.id} provisioning failed: ${String(G.error??Z.statusText)}`);if(G.status==="action_required")throw Error(`Tenant ${Y.id} provisioning requires action at ${G.actionUrl}`);if(z(Z.status===200||Z.status===201||Z.status===202,`Expected tenant create to return 200, 201, or 202, got ${Z.status}`),z(typeof G.tenantUrl==="string",`Tenant ${Y.id} response must include tenantUrl`),Q==="creation"){let B=r9(G.tenantUrl),q=X.basePath??"/api/ionite";return await E1(B,{...X.readiness,basePath:q}),{id:Y.id,baseUrl:B,basePath:q,tenantUrl:G.tenantUrl}}let W=await fetch(new URL(`${$}/${encodeURIComponent(Y.id)}`,X.baseUrl),{headers:{Authorization:`Bearer ${K}`},signal:AbortSignal.timeout(X.readiness?.requestTimeoutMs??15000)});z(W.status===200,`Expected tenant GET to return 200, got ${W.status}`);let H=await W.json();z(H.id===Y.id,`Expected tenant GET id ${Y.id}`),z(typeof H.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl"),z(typeof H.ionUrls.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl");let J=new URL(H.ionUrls.capabilitiezUrl),j="/capabilitiez";z(J.pathname.endsWith(j),"Tenant capabilitiezUrl must end in /capabilitiez");let D=J.pathname.slice(0,-j.length)||"/",P=new URL(H.ionUrls.readyzUrl);return await E1(J.origin,{...X.readiness,readyPath:`${P.pathname}${P.search}`}),{id:Y.id,baseUrl:J.origin,basePath:D,tenantUrl:typeof H.tenantUrl==="string"?H.tenantUrl:G.tenantUrl}}var s9={tenantPath:"/api/tenants"};function l2(X,$){return`${(X.tenantPath??"/api/tenants").replace(/\/+$/,"")}/${$}`}async function e9(X,$,Q,K){return N("Tenant Create",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let Y={id:Q.id,companyId:Q.companyId,companyName:Q.companyName,environmentType:Q.environmentType,...Q.configLocator?{configLocator:Q.configLocator}:{},...Q.email?{email:Q.email}:{},...Q.webhookUrl||K?{webhookUrl:Q.webhookUrl??K}:{},...Q.callbackUrl?{callbackUrl:Q.callbackUrl}:{}},Z=await $(X.tenantPath??"/api/tenants",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Y)});z(Z.status===200||Z.status===201||Z.status===202,`Expected 200, 201, or 202, got ${Z.status}`);let G=await Z.json();z(typeof G.status==="string","Expected response status");let W=G.status;if(z(["action_required","pending","processing","completed","failed"].includes(W),`Unexpected tenant status: ${W}`),W==="action_required")z(typeof G.actionUrl==="string","Expected actionUrl for action_required"),z(typeof G.requestToken==="string","Expected requestToken for action_required"),z(typeof G.expiresAt==="string","Expected expiresAt for action_required");else if(G.tenantUrl!=null)z(typeof G.tenantUrl==="string","Expected tenantUrl to be a string when provided");return{details:{httpStatus:Z.status}}})}async function $4(X,$,Q){return N("Tenant Get",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let K=await $(l2(X,Q.id),{method:"GET",headers:{"Content-Type":"application/json"}});z(K.status===200,`Expected 200, got ${K.status}`);let Y=await K.json();return z(Y.id===Q.id,"Expected id to match"),{details:{httpStatus:K.status}}})}async function Q4(X,$,Q){return N("Tenant Delete",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let K=await $(l2(X,Q.id),{method:"DELETE",headers:{"Content-Type":"application/json"}});z(K.status===204,`Expected 204, got ${K.status}`);let Y=await $(l2(X,Q.id),{method:"GET"});return z(Y.status===404,`Expected 404 after delete, got ${Y.status}`),{details:{httpStatus:K.status}}})}async function K4(X,$){return N("Tenant Creation (Validation)",async()=>{let Q={companyId:"test-company"},K=X.tenantPath??"/api/tenants",Y=await $(K,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Q)});return z(Y.status===400,`Expected 400 Bad Request for invalid payload, got ${Y.status}`),{details:{status:Y.status}}})}async function Y4(X,$){return N("Tenant Creation Rejects Malformed JSON",async()=>{let Q=X.tenantPath??"/api/tenants",K=await $(Q,{method:"POST",headers:{"Content-Type":"application/json"},body:'{"companyId":'});return z(K.status===400,`Expected 400 Bad Request for malformed JSON, got ${K.status}`),{details:{status:K.status}}})}async function Z4(X,$,Q){return N("Tenant Create Without Config Locator",async()=>{if(!Q)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let K=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),Y=`${Q.id}-unconfigured`;try{let Z=await $(K,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:Y,companyId:Q.companyId,companyName:Q.companyName,environmentType:Q.environmentType})});z(Z.status===200||Z.status===201||Z.status===202,`Expected create without configLocator to succeed, got ${Z.status}`);let G=await $(`${K}/${encodeURIComponent(Y)}`,{method:"GET"});z(G.status===200,`Expected tenant GET 200, got ${G.status}`);let W=await G.json();return z(W.configLocator==null,"Expected configLocator to remain absent after create"),{details:{httpStatus:Z.status}}}finally{await $(`${K}/${encodeURIComponent(Y)}`,{method:"DELETE"}).catch(()=>{return})}})}async function X4(X,$,Q){return N("Tenant Config Locator PUT Patch",async()=>{if(!Q?.configLocator)return{details:{skipped:!0,reason:"Test tenant config locator not provided"}};let K=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),Y=`${Q.id}-locator-patch`;try{let Z=await $(K,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:Y,companyId:Q.companyId,companyName:Q.companyName,environmentType:Q.environmentType})});z(Z.status===200||Z.status===201||Z.status===202,`Expected initial tenant create to succeed, got ${Z.status}`);let G=await $(`${K}/${encodeURIComponent(Y)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({configLocator:Q.configLocator})});z(G.status===200||G.status===201||G.status===202,`Expected configLocator PUT to return 200, 201, or 202, got ${G.status}`);let W=await $(`${K}/${encodeURIComponent(Y)}`,{method:"GET"});z(W.status===200,`Expected tenant GET 200, got ${W.status}`);let H=await W.json();return z(JSON.stringify(H.configLocator)===JSON.stringify(Q.configLocator),"Expected PUT to persist the complete configLocator"),z(H.companyName===Q.companyName,"Expected PUT patch to preserve existing tenant fields"),{details:{httpStatus:G.status}}}finally{await $(`${K}/${encodeURIComponent(Y)}`,{method:"DELETE"}).catch(()=>{return})}})}async function z4(X,$){return N("Tenant Rejects Legacy user-tenants Route",async()=>{let Q=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),K=await $(`${Q}/user-tenants?email=test@example.com`,{method:"GET",headers:{"Content-Type":"application/json"}});return z(K.status===404,`Expected 404, got ${K.status}`),{details:{status:K.status}}})}async function lQ(X){let $=performance.now(),Q={...s9,...X},K=r(Q),Y=Q.profile??"tenantManager",Z=[];if(Z.push(await G4(Q)),Z.push(await W4(Q)),Z.push(await K4(Q,K)),Z.push(await Y4(Q,K)),Y==="tenantManager")Z.push(await z4(Q,K)),Z.push(await Z4(Q,K,Q.testTenantData)),Z.push(await X4(Q,K,Q.testTenantData));if(Z.push(await e9(Q,K,Q.testTenantData,Q.webhookPath?`${T0(Q)}${Q.webhookPath}`:void 0)),Y==="tenantManager")Z.push(await $4(Q,K,Q.testTenantData)),Z.push(await Q4(Q,K,Q.testTenantData));let G=performance.now()-$,W=Z.filter((j)=>j.passed).length,H=Z.filter((j)=>!j.passed).length,J=Z.filter((j)=>j.details?.skipped).length;return{suite:"Tenant",passed:H===0,tests:Z,duration:G,summary:{total:Z.length,passed:W-J,failed:H,skipped:J}}}async function G4(X){return N("Tenant Rejects Without Workload Auth",async()=>{let $=X.tenantPath??"/api/tenants",K=await r({baseUrl:X.baseUrl,timeout:X.timeout,headers:{}})($,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:"auth-test-app",companyId:"auth-test-company",companyName:"Auth Test Company",environmentType:"POC",configLocator:{type:"vault",vaultPath:"/ecv/test-config"}})});return z(K.status===401,`Expected 401 without workload auth, got ${K.status}`),{details:{httpStatus:K.status}}})}async function W4(X){return N("Tenant Rejects Invalid Workload Token",async()=>{let $=X.tenantPath??"/api/tenants",K=await r({baseUrl:X.baseUrl,timeout:X.timeout,headers:{Authorization:"Bearer invalid-token"}})($,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:"auth-test-app",companyId:"auth-test-company",companyName:"Auth Test Company",environmentType:"POC",configLocator:{type:"vault",vaultPath:"/ecv/test-config"}})});return z(K.status===401,`Expected 401 with invalid token, got ${K.status}`),{details:{httpStatus:K.status}}})}function H4(X){let $={...s9,...X},Q=r($),K=$.profile??"tenantManager",Y=[{name:"tenant creation endpoint rejects requests without workload authentication",fn:async()=>{let Z=await G4($);if(!Z.passed)throw Error(Z.error)}},{name:"tenant creation endpoint rejects requests with invalid workload token",fn:async()=>{let Z=await W4($);if(!Z.passed)throw Error(Z.error)}},{name:"tenant creation endpoint validates required fields",fn:async()=>{let Z=await K4($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"tenant creation endpoint rejects malformed JSON",fn:async()=>{let Z=await Y4($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"tenant manager rejects legacy user-tenants route",fn:async()=>{let Z=await z4($,Q);if(!Z.passed)throw Error(Z.error)}},{name:"tenant create accepts an omitted config locator",fn:async()=>{let Z=await Z4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant PUT attaches a config locator without replacing other fields",fn:async()=>{let Z=await X4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant upsert returns valid response structure",fn:async()=>{let Z=await e9($,Q,$.testTenantData,$.webhookPath?`${T0($)}${$.webhookPath}`:void 0);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant get returns upserted tenant",fn:async()=>{let Z=await $4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}},{name:"tenant delete removes tenant",fn:async()=>{let Z=await Q4($,Q,$.testTenantData);if(!Z.passed&&!Z.details?.skipped)throw Error(Z.error)}}];if(K==="creation")return Y.filter((Z)=>["tenant creation endpoint rejects requests without workload authentication","tenant creation endpoint rejects requests with invalid workload token","tenant creation endpoint validates required fields","tenant creation endpoint rejects malformed JSON","tenant upsert returns valid response structure"].includes(Z.name));return Y}function n1(X){return typeof X.ecv==="function"?X.ecv():X.ecv}function I5(X){return`${X}-${Date.now()}-${Math.random().toString(36).slice(2,9)}`}function m5(X,$){return{id:$,companyId:$,companyName:`ECV ${$}`,environmentType:"DEV",get configLocator(){return{type:"vault",vaultPath:`${n1(X).configLocator.vaultPath}/${$}`}},get webhookUrl(){return`${n1(X).baseUrl}/webhook`}}}async function c2(X){return k0(n1(X).baseUrl,f1)}async function d2(X,$,Q){let K=(X.tenantPath??"/api/tenants").replace(/\/+$/,"");return fetch(new URL(`${K}/${encodeURIComponent($)}`,T0(X)),{method:"DELETE",headers:{Authorization:`Bearer ${Q}`},signal:AbortSignal.timeout(X.timeout??15000)})}function dQ(X){let $=m5(X,I5("ecv-lifecycle"));return{name:"tenant lifecycle exposes derived URLs and tears down tenant routing",fn:async()=>{let Q=n1(X),K=await c2(X);try{let Y=await c1({ecv:Q,baseUrl:T0(X),tenantPath:X.tenantPath,tenant:$,readiness:{requestTimeoutMs:X.timeout??15000}}),Z=(X.tenantPath??"/api/tenants").replace(/\/+$/,""),G=await fetch(new URL(`${Z}/${encodeURIComponent($.id)}`,T0(X)),{headers:{Authorization:`Bearer ${K}`},signal:AbortSignal.timeout(X.timeout??15000)});z(G.status===200,`Expected tenant GET 200, got ${G.status}`);let W=await G.json();z(typeof W.callbackUrl==="string","Tenant GET must include callbackUrl"),z(typeof W.ionUrls?.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl"),z(typeof W.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl");let H=await d2(X,$.id,K);z(H.status===204,`Expected tenant DELETE 204, got ${H.status}`);let J=await fetch(new URL(`${Z}/${encodeURIComponent($.id)}`,T0(X)),{headers:{Authorization:`Bearer ${K}`},signal:AbortSignal.timeout(X.timeout??15000)});z(J.status===404,`Expected tenant GET 404 after delete, got ${J.status}`);let j=await fetch(`${Y.baseUrl}${Y.basePath}/readyz`,{signal:AbortSignal.timeout(X.timeout??15000)});if(z(j.status===503,`Expected deleted tenant route to return 503, got ${j.status}`),Y.basePath.endsWith("/ionite")){let D=`${Y.basePath.slice(0,-7)}/readyz`,P=await fetch(`${Y.baseUrl}${D}`,{signal:AbortSignal.timeout(X.timeout??15000)});z(P.status===404,`Expected deleted tenant readiness to return 404, got ${P.status}`)}}finally{await d2(X,$.id,K).catch(()=>{return})}}}}function cQ(X){let $=m5(X,I5("ecv-isolation-a")),Q=m5(X,I5("ecv-isolation-b"));return{name:"session from one tenant cannot authenticate against another tenant",fn:async()=>{let K=n1(X),Y=await c2(X),Z=[];try{Z.push($.id);let G=await c1({ecv:K,baseUrl:T0(X),tenantPath:X.tenantPath,tenant:$,readiness:{requestTimeoutMs:X.timeout??15000}});Z.push(Q.id);let W=await c1({ecv:K,baseUrl:T0(X),tenantPath:X.tenantPath,tenant:Q,readiness:{requestTimeoutMs:X.timeout??15000}}),H={baseUrl:G.baseUrl,timeout:X.timeout,loginPath:`${G.basePath}/auth/login`,callbackPath:`${G.basePath}/auth/callback`,userPath:`${G.basePath}/auth/user`},J=r(H),j=await d1(H,J);z(j.success,j.error??"Tenant A login failed");let D=f0(j.cookies);z(D.length>0,"Tenant A login did not produce a session cookie");let P=await J(H.userPath,{headers:{Cookie:D}});z(P.status===200,`Tenant A session did not authenticate on tenant A: ${P.status}`);let q=await r({baseUrl:W.baseUrl,timeout:X.timeout})(`${W.basePath}/auth/user`,{headers:{Cookie:D}});z(q.status===401,`Tenant A session authenticated on tenant B (expected 401, got ${q.status})`)}finally{await Promise.all(Z.map((G)=>d2(X,G,Y).catch(()=>{return})))}}}}function n2(X){let $=X.profile??"tenantManager",Q,K=m5(X,I5("ecv-contract")),Y=H4({baseUrl:X.baseUrl,timeout:X.timeout,tenantPath:X.tenantPath,profile:$,webhookPath:X.webhookPath,headers:async()=>{return Q??=c2(X),{Authorization:`Bearer ${await Q}`}},testTenantData:K});if($==="creation")return Y;return[...Y,dQ(X),cQ(X)]}import{defaultLogger as nQ,list as iQ,remoteConfigSecretType as i2}from"@ionite/core";import{generateLfvEd25519KeyPair as i1,signLfvPayload as j4,stringifyLfvJwks as f5}from"@ionite/core/server";import{ionite as p5,lfvVault as o1,mirroredConfig as oQ,mirrorSecrets as aQ,tenantMirror as tQ}from"@ionite/server";import{createValidators as g5}from"@ionite/valibot";var z0={lfvServerUrl:"http://localhost:3599/api/lfv",clientId:"ecv-coordination-client",path:"/ecv/coordination/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"};class u5{values=new Map;async put(X,$,Q){this.values.set(X,$)}async take(X){let $=this.values.get(X);return this.values.delete(X),$}}async function a1(X,$,Q,K){let Y="issued_at"in $?$:{...$,issued_at:Date.now()},Z=JSON.stringify(Y),G=String($.request_id),W=await j4({privateKeyJwk:Q,requestId:G,rawBody:Z});return new Request(`http://localhost${X}`,{method:"POST",headers:{"Content-Type":"application/json","X-LFV-Request-Id":G,"X-LFV-Key-Id":K,"X-LFV-Signature":W},body:Z})}async function J4(X,$,Q){let K=String(X.request_id),Y=JSON.stringify(X),Z=await j4({privateKeyJwk:$,requestId:K,rawBody:Y});return new Response(Y,{status:200,headers:{"Content-Type":"application/json","X-LFV-Request-Id":K,"X-LFV-Key-Id":Q,"X-LFV-Signature":Z}})}async function t1(X,$,Q=1000){let K=Date.now()+Q;while(Date.now()<K){if(X())return;await new Promise((Y)=>setTimeout(Y,5))}z(X(),$)}function R1(X){let $=new Map;for(let[G,W]of Object.entries(X))$.set(G,{data:W,metadata:{path:G,version:1,created:new Date,contentType:i2.toRegistryEntry()}});let Q=new Map,K=new Set,Y=(G)=>{let W=$.get(G);if(!W)throw Error(`not found: ${G}`);return W};return{source:{type:"vault",isReady:()=>!0,ready:async()=>{},isAvailable:()=>!0,get:()=>{throw Error("not implemented")},getFullSecret:async(G)=>Y(G),getSecret:async(G)=>Y(G).data,putSecret:async(G,W)=>{let H=$.get(G);$.set(G,{data:W,metadata:{path:G,version:(H?.metadata.version??0)+1,created:new Date,contentType:i2.toRegistryEntry()}})},deleteSecret:async(G)=>{$.delete(G)},listPaths:async(G)=>Array.from($.keys()).filter((W)=>W.startsWith(G)),exists:async(G)=>$.has(G),requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async()=>{throw Error("parkKmsKey is not implemented for this test source")},addKmsKeys:async()=>{throw Error("addKmsKeys is not implemented for this test source")},revokeKmsKey:async()=>{throw Error("revokeKmsKey is not implemented for this test source")},getMetadata:async(G)=>Y(G).metadata,subscribe:async(G,W)=>{let H=Q.get(G)??new Set,J=W;return H.add(J),Q.set(G,H),J(Y(G)),()=>{H.delete(J)}},subscribeEvents:(G)=>{return K.add(G),()=>K.delete(G)}},values:$,emit(G,W,H){let J={data:W,metadata:{path:G,version:H,created:new Date,contentType:i2.toRegistryEntry()}};$.set(G,J);for(let j of Q.get(G)??[])j(J)},emitEvent(G){for(let W of K)W(G)}}}function o2(X){return{async load(){return X},async subscribe(){return},secret:{}}}function rQ(X){let $=X,Q;return{async load(){return $},async subscribe(K){return Q=K,()=>{Q=void 0}},push(K){$=K,Q?.(K)},secret:{}}}function a2(X){return{LFV_NAMED:{type:"vault",lfv:{lfvServerUrl:z0.lfvServerUrl,clientId:z0.clientId,path:X,deliveryEndpoint:z0.deliveryEndpoint,eventsEndpoint:z0.eventsEndpoint}}}}function l5(){return{tests:[{name:"RemoteConfig LFV secrets without a callback transport reject apply unless singleProcess is declared",fn:async()=>{let X={id:"ecv-lfv-transport-gate",secrets:a2("/ecv/coordination/apply-reject")},$=p5(o2(X),{validators:g5()}),Q=!1,K="";try{await $.ready(2000)}catch(Y){Q=!0,K=Y instanceof Error?Y.message:String(Y)}$.close?.(),z(Q,"LFV secrets without a callback transport must reject ion.ready()"),z(K.includes("callbackTransport")&&K.includes("singleProcess"),`apply rejection should name callbackTransport and the singleProcess opt-out (got: ${K})`)}},{name:"RemoteConfig LFV secrets apply when singleProcess is declared or a callback transport is wired",fn:async()=>{let X={id:"ecv-lfv-transport-ok",secrets:a2("/ecv/coordination/apply-ok")},$=p5(o2(X),{validators:g5(),lfv:{singleProcess:!0}});await $.ready(2000),z($.isReady(),"singleProcess declaration must allow LFV apply without a transport"),$.close?.();let Q=p5(o2(X),{validators:g5(),lfv:{callbackTransport:new u5}});await Q.ready(2000),z(Q.isReady(),"a wired callback transport must allow LFV apply"),Q.close?.()}},{name:"a live RemoteConfig that introduces LFV without a transport keeps last-known-good",fn:async()=>{let X=rQ({id:"ecv-lfv-transport-lkg",secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),$=p5(X,{validators:g5()});await $.ready(2000),z($.isReady(),"initial HTTP vault secrets apply without a callback transport"),z($.secrets?.listSecretsSources().includes("primary")===!0,"last-known-good keeps the HTTP vault source"),X.push({id:"ecv-lfv-transport-lkg",secrets:a2("/ecv/coordination/apply-lkg")}),await new Promise((K)=>setTimeout(K,30)),z($.isReady(),"the instance stays ready after a rejected LFV live push"),z($.secrets?.listSecretsSources().includes("primary")===!0,"last-known-good HTTP vault source is retained"),z($.secrets?.listSecretsSources().includes("LFV_NAMED")!==!0,"the untransportable LFV source is not applied");let Q=$.configStatus.lastError?.message??"";z(Q.includes("callbackTransport"),`configStatus.lastError should name the missing callback transport (got: ${Q})`),$.close?.()}},{name:"distributed LFV callbacks resolve an OTP waiter on a different replica",fn:async()=>{let X=new u5,{privateJwk:$,publicJwks:Q}=await i1("ecv-coordination-kid"),K={...z0,verifyJwks:f5(Q),deliveryTimeout:1000},Y=o1(K,X),Z=o1(K,X),G=globalThis.fetch;globalThis.fetch=async(W,H)=>{let J=typeof W==="string"?W:W instanceof URL?W.toString():W.url,j=JSON.parse(String(H?.body));if(J.endsWith("/otp")){let D=await a1(z0.deliveryEndpoint,{type:"otp",request_id:j.request_id,otp:`otp-${j.request_id}`,action:j.action,path:j.path},$,"ecv-coordination-kid");return w((await Z.handleDelivery(D)).status,200,"ingress replica accepts signed OTP"),Response.json({status:"pending"},{status:202})}if(J.endsWith("/secrets/request"))return J4({request_id:j.request_id,status:"completed",path:j.path,data:{data:{token:"coordinated"},metadata:{path:j.path,version:1}}},$,"ecv-coordination-kid");throw Error(`Unexpected LFV request: ${J}`)};try{let W=await Y.getSecret(z0.path);w(W.token,"coordinated","requesting replica receives callback through transport")}finally{globalThis.fetch=G,Y.close?.(),Z.close?.()}}},{name:"mirrored LFV self-redeems on the callback replica without a transport or waiter",fn:async()=>{let{privateJwk:X,publicJwks:$}=await i1("ecv-self-redeem-kid"),Q=o1({...z0,verifyJwks:f5($),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),K=globalThis.fetch,Y=0,Z,G=await Q.subscribePassive(z0.path,(W)=>{Z=W});globalThis.fetch=async(W,H)=>{let J=JSON.parse(String(H?.body));return Y+=1,J4({request_id:J.request_id,status:"completed",path:J.path,data:{data:{token:"self-redeemed"},metadata:{path:J.path,version:1}}},X,"ecv-self-redeem-kid")};try{let W=crypto.randomUUID(),H=await Q.handleDelivery(await a1(z0.deliveryEndpoint,{type:"otp",request_id:W,otp:`otp-${W}`,action:"read_secret",path:z0.path},X,"ecv-self-redeem-kid"));w(H.status,200,"the callback replica accepts and redeems the OTP"),w(Y,1,"the callback replica redeems exactly once without a mailbox"),w(Z?.data.token,"self-redeemed","the pulled value is published to the mirror subscriber")}finally{G(),globalThis.fetch=K,Q.close?.()}}},{name:"already-used self-redeem callback is terminally accepted and its replay claim survives",fn:async()=>{let{privateJwk:X,publicJwks:$}=await i1("ecv-used-otp-kid"),Q=o1({...z0,verifyJwks:f5($),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),K=globalThis.fetch;globalThis.fetch=async()=>{return Response.json({error:"otp_already_used"},{status:409})};let Y=crypto.randomUUID(),Z={type:"otp",request_id:Y,otp:`otp-${Y}`,action:"read_secret",path:z0.path,issued_at:Date.now()};try{let G=await Q.handleDelivery(await a1(z0.deliveryEndpoint,Z,X,"ecv-used-otp-kid"));w(G.status,200,"already-used OTP is a terminal accepted callback, not a retryable 503");let W=await Q.handleDelivery(await a1(z0.deliveryEndpoint,Z,X,"ecv-used-otp-kid"));w(W.status,401,"the terminal callback keeps its local replay claim")}finally{globalThis.fetch=K,Q.close?.()}}},{name:"distributed envelopes are re-verified and do not cross a JWKS trust boundary",fn:async()=>{let X=new u5,$=await i1("trusted-kid"),Q=await i1("untrusted-kid"),K=o1({...z0,verifyJwks:f5($.publicJwks),deliveryTimeout:80},X),Y=globalThis.fetch,Z="";globalThis.fetch=async(G,W)=>{return Z=JSON.parse(String(W?.body)).request_id??"",Response.json({status:"pending"},{status:202})};try{let G=K.getSecret(z0.path,{timeout:80});await t1(()=>Z.length>0,"source must issue an OTP request");let W=Z,H=await a1(z0.deliveryEndpoint,{type:"otp",request_id:W,otp:"attacker-otp",action:"read_secret",path:z0.path},Q.privateJwk,"untrusted-kid"),J=await H.text();await X.put(W,{kind:"delivery",key:K.callbackKey,rawBody:J,headers:{requestId:W,keyId:"untrusted-kid",signature:H.headers.get("X-LFV-Signature")??""}},1000);let j=!1;try{await G}catch{j=!0}z(j,"untrusted distributed OTP must not resolve the waiter")}finally{globalThis.fetch=Y,K.close?.()}}},{name:"OEM mirror converges values and removes revoked material",fn:async()=>{let X=R1({"payments/api":{token:"v1"}}),$=R1({}),Q=aQ({from:X.source,to:$.source,paths:["payments/api"],mapPath:(K)=>`tenants/customer-a/${K}`});try{await Q.ready(1000);let K="tenants/customer-a/payments/api";w(($.values.get(K)?.data).token,"v1","initial value is re-stored"),z(!$.values.has(`${K}/__ionite_mirror`),"mirrors store no provenance sidecar");let Y=$.values.get(K)?.metadata.version;X.emit("payments/api",{token:"v1"},1),await new Promise((Z)=>setTimeout(Z,20)),w($.values.get(K)?.metadata.version,Y,"identical replay does not mint a new destination version"),X.emit("payments/api",{token:"v2"},2),await t1(()=>$.values.get(K)?.data?.token==="v2","rotation must update mirror"),X.emitEvent({path:"payments/api",event:"revoked"}),await t1(()=>!$.values.has(K),"revocation must remove mirrored value")}finally{await Q.close()}}},{name:"mirroredConfig loads only from the durable destination",fn:async()=>{let X=R1({"/authoritative/root":{id:"root-v1",readyz:{enabled:!0}}}),$=R1({}),Q=oQ({from:X.source,fromPath:"/authoritative/root",to:$.source,path:"/mirror/root"});try{await Q.mirror.ready(1000),w((await Q.load()).id,"root-v1","config loads from the mirrored destination"),X.emit("/authoritative/root",{id:"root-v2"},2),await t1(()=>$.values.get("/mirror/root")?.data?.id==="root-v2","upstream changes must converge into the destination")}finally{await Q.secret.close?.()}}},{name:"tenantMirror mirrors and purges one tenant without store-owned hooks",fn:async()=>{let X=R1({"/tenant/acme/config":{id:"acme-config"}}),$=R1({}),Q={id:"acme",companyId:"acme",companyName:"Acme",environmentType:"DEV",status:"completed",tenantUrl:"https://app.example/acme",createdAt:new Date,updatedAt:new Date,configLocator:{type:"vault",vaultPath:"/tenant/acme/config"}},Y=tQ({log:nQ},{store:{get:async(Z)=>Z===Q.id?Q:void 0,list:async()=>iQ(1,[Q],0,void 0),upsert:async()=>Q,delete:async()=>1,getIon:async()=>{return}},to:$.source,path:({id:Z})=>`/mirrors/${Z}/config`,sourceFromLocator:()=>X.source});try{await Y.ensure("acme"),await t1(()=>$.values.get("/mirrors/acme/config")?.data?.id==="acme-config","tenant config must be mirrored"),z(!$.values.has("/mirrors/acme/config/__ionite_mirror"),"mirrors store no provenance sidecar"),await Y.purge("acme"),z(!$.values.has("/mirrors/acme/config"),"purge removes the tenant config")}finally{await Y.close()}}}]}}async function sQ(){let X=performance.now(),$=[];for(let Z of l5().tests)$.push(await N(Z.name,async()=>{await Z.fn();return}));let Q=performance.now()-X,K=$.filter((Z)=>!Z.passed).length,Y=$.filter((Z)=>Z.details?.skipped).length;return{suite:"Vault coordination",passed:K===0,tests:$,duration:Q,summary:{total:$.length,passed:$.length-K-Y,failed:K,skipped:Y}}}import{closeAllVaultSockets as w4,setVaultWebSocketCommandTimeout as D4,vault as A0,vaultSocketDiagnostics as d5}from"@ionite/server";import{WebSocketServer as eQ}from"ws";var $7={"tok-a":"CLT-A","tok-b":"CLT-B","tok-solo":"CLT-SOLO"};function Q7(X){return typeof X==="string"?$7[X]:void 0}function c5(X,$){return`${X}::${$}`}function K7(X){let $=X["x-vault-token"],Q=Array.isArray($)?$[0]:$;if(Q)return Q;let K=X.authorization,Y=Array.isArray(K)?K[0]:K;if(Y)return Y.replace(/^Bearer\s+/i,"");return}async function Y7(){let X=new Map,$=new Set,Q=new Set,K=new Set,Y=!1,Z=new Map,G=0,W=new eQ({port:0,verifyClient:(B,q)=>{let L=K7(B.req.headers);if(L&&K.has(L)){q(!1,401,"revoked credential");return}q(!0)}});await new Promise((B)=>W.once("listening",B));let H=W.address();if(typeof H==="string"||!H)throw Error("failed to bind test vault ws server");let J=`ws://127.0.0.1:${H.port}`,j=(B,q)=>{if(B.readyState===B.OPEN)B.send(JSON.stringify(q))},D=(B,q,L,O)=>{let M=X.get(c5(B,q));if(!M)return;return{type:"secret",subscription_id:L,client_id:B,event:O,path:q,subscription:q,data:M.data,metadata:Y?{path:q,created_time:new Date().toISOString()}:{path:q,version:M.version,created_time:new Date().toISOString()}}};return W.on("connection",(B)=>{G+=1,Q.add(B),Z.set(B,new Map),B.on("message",(q)=>{let L;try{L=JSON.parse(q.toString())}catch{return}let O=L.type,M=Q7(L.token);if(O==="subscribe"){let F=String(L.subscription_id??""),T=Array.isArray(L.paths)?String(L.paths[0]):"";if(!M){j(B,{type:"error",subscription_id:F,error:"invalid token"});return}Z.get(B)?.set(F,{clientId:M,path:T});let x=D(M,T,F,"subscribed");if(x)j(B,x);return}if(O==="unsubscribe"){Z.get(B)?.delete(String(L.subscription_id??""));return}if(O==="command"){let F=String(L.id??""),T=String(L.path??"");if($.has(T))return;if(!M){j(B,{type:"command_result",id:F,ok:!1,error:"invalid token",code:"auth",status:403});return}if(L.command==="read_secret"){let x=X.get(c5(M,T));if(!x){j(B,{type:"command_result",id:F,ok:!1,error:"not found",code:"not_found",status:404});return}j(B,{type:"command_result",id:F,ok:!0,result:{path:T,data:x.data,metadata:Y?{path:T,created_time:new Date().toISOString()}:{path:T,version:x.version,created_time:new Date().toISOString()}}});return}j(B,{type:"command_result",id:F,ok:!0,result:{}});return}}),B.on("close",()=>{Q.delete(B),Z.delete(B)}),j(B,{type:"connected"})}),{url:J,connectionCount:()=>G,openCount:()=>Q.size,setSecret:(B,q,L)=>{let O=c5(B,q),M=(X.get(O)?.version??0)+1;X.set(O,{data:L,version:M})},pushUpdate:(B,q,L)=>{let O=c5(B,q),M=(X.get(O)?.version??0)+1;X.set(O,{data:L,version:M});for(let[F,T]of Z)for(let[x,b]of T){if(b.clientId!==B||b.path!==q)continue;let p=D(B,q,x,"updated");if(p)j(F,p)}},dropSockets:()=>{for(let B of Array.from(Q))B.terminate()},ignore:$,setOmitVersions:(B)=>{Y=B},revokeToken:(B)=>{K.add(B)},close:()=>new Promise((B)=>{for(let O of W.clients)O.terminate();let q=!1,L=()=>{if(q)return;q=!0,B()};W.close(()=>L()),setTimeout(L,250)})}}function B4(X){return new Promise(($)=>setTimeout($,X))}async function m0(X,$=5000,Q=50){let K=Date.now();while(Date.now()-K<$){if(X())return!0;await B4(Q)}return X()}async function a0(X){let $=await Y7();try{await X($)}finally{w4(),D4(30000),await $.close()}}function n5(){return{tests:[{name:"multiple sources on one URL share a single socket and demux by subscription_id",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-A","/shared",{who:"A"}),$.setSecret("CLT-B","/shared",{who:"B"});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-a",clientId:"CLT-A"}}),K=A0({type:"vault",websocket:{url:$.url,token:"tok-b",clientId:"CLT-B"}}),Y=[],Z=[],G=await Q.subscribe("/shared",(j)=>Y.push(j.data)),W=await K.subscribe("/shared",(j)=>Z.push(j.data)),H=d5();w(H.sockets,1,"exactly one socket per URL across both sources"),w(H.subscriptions,2,"two registered subscriptions on the shared socket"),w($.connectionCount(),1,"server accepted exactly one connection"),z(Y.some((j)=>j?.who==="A"),"client A receives its own tenant-scoped secret"),z(Z.some((j)=>j?.who==="B"),"client B receives a different secret on the same path"),G(),W();let J=await m0(()=>d5().sockets===0&&$.openCount()===0);z(J,"socket idle-closes after the last subscription is removed")})}},{name:"per-frame auth rejects an invalid token (fail-fast subscribe throws)",fn:async()=>{await a0(async($)=>{let Q=A0({type:"vault",websocket:{url:$.url,token:"not-valid",clientId:"CLT-A"}}),K=!1;try{await Q.subscribe("/shared",()=>{},{mode:"fail-fast"})}catch{K=!0}z(K,"subscribe with an invalid per-frame token must reject")})}},{name:"live update is delivered and deduplicated by version",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-SOLO","/live",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=[],Y=await Q.subscribe("/live",(Z)=>K.push(Z.data.v));z(await m0(()=>K.includes(1)),"initial value delivered"),$.pushUpdate("CLT-SOLO","/live",{v:2}),z(await m0(()=>K.includes(2)),"updated value delivered"),Y()})}},{name:"reconnect replays active subscriptions after an unexpected socket drop",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-SOLO","/recon",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=[],Y=await Q.subscribe("/recon",(G)=>K.push(G.data.v));z(await m0(()=>K.includes(1)),"initial value delivered before drop"),$.setSecret("CLT-SOLO","/recon",{v:2}),$.dropSockets();let Z=await m0(()=>K.includes(2),8000);z(Z,"subscription replayed and newer secret delivered after reconnect"),Y()})}},{name:"a command with no reply rejects after the per-command timeout",fn:async()=>{await a0(async($)=>{D4(250),$.ignore.add("/never");let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=!1;try{await Q.getFullSecret("/never")}catch{K=!0}z(K,"an unanswered websocket command must reject on timeout")})}},{name:"close() releases subscriptions and idle-closes the shared socket",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-SOLO","/closing",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}});await Q.subscribe("/closing",()=>{}),w(d5().sockets,1,"socket open while subscribed"),await Q.close?.();let K=await m0(()=>d5().sockets===0&&$.openCount()===0);z(K,"close() releases the subscription and the socket idle-closes")})}},{name:"version-less deliveries are never deduplicated (server omits metadata.version)",fn:async()=>{await a0(async($)=>{$.setOmitVersions(!0),$.setSecret("CLT-SOLO","/nov",{n:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}),K=[],Y=await Q.subscribe("/nov",(G)=>K.push(G.data.n));z(await m0(()=>K.includes(1)),"initial version-less value delivered");let Z=K.length;$.pushUpdate("CLT-SOLO","/nov",{n:2}),$.pushUpdate("CLT-SOLO","/nov",{n:2}),z(await m0(()=>K.length>=Z+2),"both version-less updates must be delivered (no version → no dedup)"),z(K.slice(Z,Z+2).every((G)=>G===2),"the delivered version-less updates carry the pushed value"),Y()})}},{name:"a throwing onChange is contained; sibling subscriptions still deliver",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-A","/boom",{v:1}),$.setSecret("CLT-B","/safe",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-a",clientId:"CLT-A"}}),K=A0({type:"vault",websocket:{url:$.url,token:"tok-b",clientId:"CLT-B"}}),Y=[],Z=await Q.subscribe("/boom",()=>{throw Error("intentional onChange failure")}),G=await K.subscribe("/safe",(W)=>Y.push(W.data.v));z(await m0(()=>Y.includes(1)),"sibling receives initial value despite the throwing peer"),$.pushUpdate("CLT-A","/boom",{v:2}),$.pushUpdate("CLT-B","/safe",{v:2}),z(await m0(()=>Y.includes(2)),"sibling still receives updates after the throwing subscription fired"),Z(),G()})}},{name:"reconnect rotates to a second credential when the first is revoked",fn:async()=>{await a0(async($)=>{$.setSecret("CLT-A","/rot-a",{v:1}),$.setSecret("CLT-B","/rot-b",{v:1});let Q=A0({type:"vault",websocket:{url:$.url,token:"tok-a",clientId:"CLT-A"}}),K=A0({type:"vault",websocket:{url:$.url,token:"tok-b",clientId:"CLT-B"}}),Y=[],Z=await Q.subscribe("/rot-a",()=>{}),G=await K.subscribe("/rot-b",(H)=>Y.push(H.data.v));z(await m0(()=>Y.includes(1)),"initial delivery before the credential is revoked"),$.revokeToken("tok-a"),$.setSecret("CLT-B","/rot-b",{v:2}),$.dropSockets();let W=await m0(()=>Y.includes(2),20000);z(W,"reconnect recovered via the second (still-valid) credential and replayed subscriptions"),Z(),G()})}},{name:"wait-mode subscribe rejects when the socket is disposed before the first secret",fn:async()=>{await a0(async($)=>{let K=A0({type:"vault",websocket:{url:$.url,token:"tok-solo",clientId:"CLT-SOLO"}}).subscribe("/never-arrives",()=>{},{mode:"wait"}),Y=!1,Z="";K.then(()=>{},(G)=>{Y=!0,Z=G instanceof Error?G.message:String(G)}),await B4(200),w4();try{await K}catch(G){Y=!0,Z=G instanceof Error?G.message:String(G)}z(Y,"wait-mode subscribe must reject when the socket is disposed (no hang)"),z(Z.toLowerCase().includes("disposed")||Z.toLowerCase().includes("interrupted"),`rejection should explain the socket teardown (got: ${Z})`)})}}]}}async function Z7(){let X=performance.now(),{tests:$}=n5(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Vault websocket transport",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}function i5(X,$){return z(typeof X==="string"&&X.length>0,`${$} must be configured`),X}function v1(X,$){return z(typeof X==="object"&&X!==null&&!Array.isArray(X),`${$} must be an object`),X}function P4(X,$){return z(typeof X==="boolean",`${$} must be a boolean`),X}function X7(X,$){return z(Array.isArray(X),`${$} must be an array`),X}function z7(){return{"~standard":{validate:(X)=>{if(typeof X!=="object"||X===null)return{issues:[{message:`Expected object, got ${typeof X}`}]};let $=X,Q=[];if(typeof $.kty!=="string")Q.push({message:"Expected kty to be string",path:["kty"]});if(typeof $.kid!=="string")Q.push({message:"Expected kid to be string",path:["kid"]});if(Q.length>0)return{issues:Q};return{value:X}}}}}function q4(){return{"~standard":{validate:(X)=>{if(typeof X!=="object"||X===null)return{issues:[{message:`Expected object, got ${typeof X}`}]};let $=X,Q=[];if(typeof $.valid!=="boolean")Q.push({message:"Expected valid to be boolean",path:["valid"]});if(Q.length>0)return{issues:Q};return{value:X}}}}}var F4={validatePath:"/api/workload/validate",jwksPath:"/api/workload/jwks"};async function t2(X){if(X.validToken)return X.validToken;return k0(V1(X.ecvUrl,"Workload validation ecvUrl when validToken is absent"))}async function L4(X,$){return N("Workload JWKS Endpoint",async()=>{let Q=await $(i5(X.jwksPath,"jwksPath"));z(Q.status===200,`Expected 200 OK, got ${Q.status}`);let K=v1(await Q.json(),"JWKS response"),Y=X7(K.keys,"JWKS keys"),Z=z7();for(let G of Y)r0(G,Z);return{details:{keyCount:Y.length,algorithms:Y.map((G)=>v1(G,"JWKS key").alg).filter((G)=>typeof G==="string")}}})}async function V4(X,$,Q){return N("Workload Validate Endpoint",async()=>{let K=await $(i5(X.validatePath,"validatePath"),{method:"POST",headers:{Authorization:`Bearer ${Q}`}});z(K.status===200,`Expected 200 OK for valid token, got ${K.status}`);let Y=v1(await K.json(),"Token validation response"),Z=q4();r0(Y,Z);let G=P4(Y.valid,"Validation result valid");z(G===!0,"Token should be valid");let W=new Set(["valid","expiresAt"]),H=Object.keys(Y).filter((J)=>!W.has(J));return z(H.length===0,`Validate response leaked fields: ${H.join(", ")}`),{details:{valid:G,expiresAt:Y.expiresAt}}})}async function N4(X,$){return N("Workload Validate Endpoint (Invalid Token)",async()=>{let Q=await $(i5(X.validatePath,"validatePath"),{method:"POST",headers:{Authorization:"Bearer invalid.token.here"}});z(Q.status===401,`Expected 401 Unauthorized for invalid token, got ${Q.status}`);let K=v1(await Q.json(),"Invalid token validation response"),Y=q4();r0(K,Y);let Z=P4(K.valid,"Validation result valid");z(Z===!1,"Invalid token should not validate"),z(K.error==="invalid_token",`Expected generic 'invalid_token' error, got ${String(K.error)}`);let G=new Set(["valid","error"]),W=Object.keys(K).filter((H)=>!G.has(H));return z(W.length===0,`Invalid-token response leaked fields: ${W.join(", ")}`),{details:{valid:Z,error:K.error}}})}async function O4(X,$){return N("Workload Validate Endpoint (No Auth)",async()=>{let Q=await $(i5(X.validatePath,"validatePath"),{method:"POST"});return z(Q.status===401,`Expected 401 Unauthorized for missing auth, got ${Q.status}`),{details:{status:Q.status,requiresAuth:!0}}})}async function M4(X,$){return N("Workload Authentication (Whoami)",async()=>{let K=`${V1(X.ecvUrl,"Workload validation ecvUrl")}/api/whoami`,Y=await globalThis.fetch(K,{headers:{Authorization:`Bearer ${$}`}});if(Y.status===404)return{details:{skipped:!0,reason:"ECV whoami endpoint not available"}};z(Y.status===200,`Expected 200 OK, got ${Y.status}`);let Z=v1(await Y.json(),"Whoami response"),G=v1(Z.workload,"Whoami workload");return{details:{workloadId:G.workloadId,clientId:G.clientId}}})}async function G7(X){let $=performance.now(),Q={...F4,...X},K=r(Q),Y=[];Y.push(await L4(Q,K)),Y.push(await O4(Q,K)),Y.push(await N4(Q,K));let Z=await t2(Q);Y.push(await V4(Q,K,Z)),Y.push(await M4(Q,Z));let G=performance.now()-$,W=Y.filter((j)=>j.passed).length,H=Y.filter((j)=>!j.passed).length,J=Y.filter((j)=>j.details?.skipped).length;return{suite:"Workload",passed:H===0,tests:Y,duration:G,summary:{total:Y.length,passed:W-J,failed:H,skipped:J}}}function o5(X){let $={...F4,...X},Q=r($);return[{name:"JWKS endpoint returns valid keys",fn:async()=>{let K=await L4($,Q);if(!K.passed)throw Error(K.error)}},{name:"validate endpoint rejects missing auth",fn:async()=>{let K=await O4($,Q);if(!K.passed)throw Error(K.error)}},{name:"validate endpoint rejects invalid tokens",fn:async()=>{let K=await N4($,Q);if(!K.passed)throw Error(K.error)}},{name:"validate endpoint accepts valid token",fn:async()=>{let K=await t2($),Y=await V4($,Q,K);if(!Y.passed)throw Error(Y.error)}},{name:"whoami endpoint works with workload authentication",fn:async()=>{let K=await t2($),Y=await M4($,K);if(!Y.passed&&!Y.details?.skipped)throw Error(Y.error)}}]}var J7="/api/ionite";function j7(X){let $=typeof X.basePath==="function"?X.basePath():X.basePath;return X.capabilitiezPath??`${$??J7}/capabilitiez`}function s2(X){let $=typeof X.ecv==="function"?X.ecv():X.ecv;if(!$)throw Error("ECV server handle is required");return $.baseUrl}function T4(X,$){z(typeof X==="object"&&X!==null,`capabilities.${$} must be an object`);let Q=X;if(z(typeof Q.module==="string",`capabilities.${$}.module must be a string`),z(typeof Q.active==="boolean",`capabilities.${$}.active must be a boolean`),z(typeof Q.ready==="boolean",`capabilities.${$}.ready must be a boolean`),z(typeof Q.testable==="boolean",`capabilities.${$}.testable must be a boolean`),Q.urls!==void 0)z(Array.isArray(Q.urls)&&Q.urls.every((K)=>typeof K==="string"),`capabilities.${$}.urls must be a string array when provided`);if(Q.reason!==void 0)z(typeof Q.reason==="string",`capabilities.${$}.reason must be a string when provided`);return Q}function r2(X,$="manifest"){if(Array.isArray(X)){X.forEach((K,Y)=>r2(K,`${$}[${Y}]`));return}if(typeof X!=="object"||X===null)return;let Q=new Set(["apiKey","clientSecret","privateKey","remoteConfig","signingKey","token"]);for(let[K,Y]of Object.entries(X))z(!Q.has(K),`capabilitiez manifest must not expose sensitive field ${$}.${K}`),r2(Y,`${$}.${K}`)}async function e2(X){let $=r(X),Q=await k0(s2(X),x2),K=await $(j7(X),{headers:{Authorization:`Bearer ${Q}`}});z(K.status===200,`Expected 200 from capabilitiez endpoint, got ${K.status}. Ensure RemoteConfig includes capabilitiez: true and workload auth is configured.`);let Y=await K.json();z(typeof Y.sdk==="object"&&Y.sdk!==null,"capabilitiez manifest must include sdk"),z(typeof Y.sdk.version==="string","capabilitiez manifest sdk.version must be a string"),z(Number.isInteger(Y.sdk.catalogVersion)&&Y.sdk.catalogVersion>0,"capabilitiez manifest sdk.catalogVersion must be a positive integer"),z(typeof Y.basePath==="string","capabilitiez manifest must include basePath:string"),z(typeof Y.lfvCallbackTransport==="boolean","capabilitiez manifest must include lfvCallbackTransport:boolean"),z(typeof Y.urls==="object"&&Y.urls!==null,"capabilitiez manifest must include urls"),z(typeof Y.capabilities==="object"&&Y.capabilities!==null,"capabilitiez manifest must include capabilities");for(let Z of W7)T4(Y.capabilities[Z.key],Z.key);for(let Z of Object.keys(Y.capabilities))T4(Y.capabilities[Z],Z);return r2(Y),Y}function w7(X){return X.toLowerCase().replace(/§\d+(?:\.\d+)*/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function r1(X,$,Q,K,Y=`${X}.${w7(Q)}`){return{id:Y,unit:X,suite:$,name:Q,title:`ECV(${Y}) ${Q}`,run:K}}function a5(X,$){return z(X!==void 0,`${$} requires the capabilitiez manifest`),X}function t5(X){let $;return(Q)=>{return $??=X(Q),$}}function j0(X,$,Q,K){for(let Y of K)X.push(r1($,Q,Y.name,async()=>Y.fn()))}function b0(X,$,Q,K,Y){K.forEach((Z,G)=>{X.push(r1($,Q,Z.name,async(W)=>{let H=Y(a5(W,$))[G];z(H!==void 0,`${$} test "${Z.name}" could not be resolved`),await H.fn()}))})}function D7(X){j0(X,"sdk.config-retry","SDK / Config retry policy",M2()),j0(X,"sdk.config-lifecycle","SDK / Config lifecycle",N2()),j0(X,"sdk.config-boundary","SDK / Config boundary",O2()),j0(X,"sdk.harness","SDK / Harness",_2()),j0(X,"sdk.authz-closure","SDK / Authz closure",J5().tests),j0(X,"sdk.lfv-callbacks","SDK / LFV callbacks",D5().tests),j0(X,"sdk.secrets-hardening","SDK / Secrets hardening",T5().tests),j0(X,"sdk.vault-coordination","SDK / Vault coordination",l5().tests),j0(X,"sdk.vault-websocket","SDK / Vault WebSocket",n5().tests),j0(X,"sdk.iam-delta","SDK / IAM delta",L5().tests),j0(X,"sdk.kms","SDK / KMS",V5().tests),j0(X,"sdk.otel","SDK / OpenTelemetry",O5().tests),j0(X,"sdk.protocol-surface","SDK / Protocol surface",M5().tests),j0(X,"sdk.sso-id-token-verification","SDK / SSO ID token verification",b5().tests),j0(X,"sdk.sso-control-contract","SDK / SSO control contract",_5().tests),j0(X,"sdk.sso-token-client-auth","SDK / SSO token client auth",k5().tests),j0(X,"sdk.sso-session-window","SDK / SSO session window",C5().tests)}function $$(X){return[r1("meta.run","ECV metadata",H9(H7).replace(/^ECV\(meta\.run\)\s*/,""),async($,Q)=>J9(a5($,"meta.run"),X(),Q),"meta.run"),r1("meta.parity","ECV metadata","declared claims match runtime-testable capabilities",async($,Q)=>j9(a5($,"meta.parity"),X(),Q),"meta.parity")]}function B7(X,$,Q={}){let K=[],Y=x5({...X}),Z=t5((G)=>x5({...X,loginPath:G.urls.sso.loginUrl,callbackPath:G.urls.sso.callbackUrl,userPath:G.urls.sso.userUrl,logoutPath:G.urls.sso.logoutUrl,backChannelLogoutPath:G.urls.sso.logoutBackChannelUrl,tokenPath:G.urls.sso.loginUrl.replace(/\/login$/,"/token"),refreshPath:G.urls.sso.refreshUrl}));if($==="sso.login"){if(b0(K,"sso.login","SSO / Core login",Y.tests,(G)=>Z(G).tests),Q.expectedCallbackErrorPattern){let G=Q.expectedCallbackErrorPattern;b0(K,"sso.login","SSO / Core login",Y.ext.createCallbackErrorSurfacedTests(G),(W)=>Z(W).ext.createCallbackErrorSurfacedTests(G))}}if($==="sso.jit")b0(K,"sso.jit","SSO / JIT",Y.ext.createJITTests(),(G)=>Z(G).ext.createJITTests());if($==="sso.logout")b0(K,"sso.logout","SSO / Logout",Y.ext.createLogoutTests(),(G)=>Z(G).ext.createLogoutTests());if($==="sso.back_channel_logout")b0(K,"sso.back_channel_logout","SSO / Back-channel logout",Y.ext.createBackChannelLogoutTests(),(G)=>Z(G).ext.createBackChannelLogoutTests());return K}function P7(X,$){let Q=[],K=w5({...X}),Y=t5((Z)=>w5({...X,customerPath:Z.urls.ciam.customerUrl,magicLinkPath:Z.urls.ciam.magicLinkUrl,magicLinkLoginPath:Z.urls.ciam.magicLinkLoginUrl,logoutPath:Z.urls.ciam.logoutUrl,customerProvidersPath:Z.urls.ciam.customerProvidersUrl,customerLoginPath:Z.urls.ciam.customerLoginUrl}));if($==="ciam.customer")b0(Q,"ciam.customer","CIAM / Customer",K.ext.createCustomerTests(),(Z)=>Y(Z).ext.createCustomerTests());if($==="ciam.magic_link")b0(Q,"ciam.magic_link","CIAM / Magic link",K.ext.createMagicLinkTests(),(Z)=>Y(Z).ext.createMagicLinkTests());if($==="ciam.oidc_login")b0(Q,"ciam.oidc_login","CIAM / OIDC login",K.ext.createOidcLoginTests(),(Z)=>Y(Z).ext.createOidcLoginTests());if($==="ciam.logout")b0(Q,"ciam.logout","CIAM / Logout",K.ext.createLogoutTests(),(Z)=>Y(Z).ext.createLogoutTests());return Q}function q7(X,$){let Q=[],K=()=>s2(X),Y=F5({...X,ecvUrl:K,ion:X.ion}),Z=t5((G)=>F5({...X,ecvUrl:K,scimPath:G.urls.iam.basePath,groupsInboundPath:G.urls.iam.groupsUrl,ion:X.ion}));if($==="iam.users")b0(Q,"iam.users","IAM / Users",Y.tests,(G)=>Z(G).tests);if($==="iam.groups")b0(Q,"iam.groups","IAM / Groups outbound",Y.ext.createGroupsOutboundTests(),(G)=>Z(G).ext.createGroupsOutboundTests()),b0(Q,"iam.groups","IAM / Groups inbound",Y.ext.createGroupsInboundTests(),(G)=>Z(G).ext.createGroupsInboundTests());return Q}function F7(X,$){let Q=[];if($==="workload.incoming"){let K=()=>s2(X),Y=o5({...X,ecvUrl:K}),Z=t5((G)=>o5({...X,ecvUrl:K,validatePath:G.urls.workload.validateUrl,jwksPath:G.urls.workload.jwksUrl}));b0(Q,"workload.incoming","Workload / Incoming",Y,Z)}if($==="workload.outgoing")Q.push(r1("workload.outgoing","Workload / Outgoing","runtime capability is testable",async(K)=>{let Y=a5(K,"workload.outgoing");z(Y.capabilities["workload.outgoing"].testable,Y.capabilities["workload.outgoing"].reason??"workload.outgoing is not testable")}));return Q}function L7(X){let $=[],Q={ecv:X.ecv,timeout:X.timeout},K=j5(Q);return b0($,"authz.oaa","Authz / OAA",K,()=>K),$}function Q$(){let X=[];return D7(X),X}function K$(X,$={}){let Q=[],K=n2({baseUrl:$.baseUrl??X.baseUrl,timeout:X.timeout,skipTlsVerify:X.skipTlsVerify,tenantPath:$.tenantPath,profile:$.profile,webhookPath:$.webhookPath,ecv:()=>{let Y=typeof X.ecv==="function"?X.ecv():X.ecv;return z(Y!==void 0,"ecv.tenants() requires the ECV server handle returned by setup()"),Y}});return j0(Q,"tenant.management","Tenant / Management",K),Q}function Y$(X,$,Q={}){if($.startsWith("sso."))return B7(X,$,Q);if($.startsWith("ciam."))return P7(X,$);if($.startsWith("iam."))return q7(X,$);if($.startsWith("workload."))return F7(X,$);return L7(X)}import{GenericContainer as _4,Wait as V7}from"testcontainers";async function r5(X,$,Q){let K=await fetch(`${X.baseUrl}/__ecv${$}`,{...Q,headers:{Authorization:`Bearer ${X.env.ION_VAULT_TOKEN}`,...Q?.body?{"Content-Type":"application/json"}:{},...Q?.headers}});if(!K.ok){let Y=await K.text();throw Error(`ECV control API ${Q?.method??"GET"} ${$} failed: ${K.status} ${Y}`)}if(K.status===204)return;return await K.json()}function x4(X){return{...X,reset:()=>r5(X,"/reset",{method:"POST"}),setTestUser:($)=>r5(X,"/test-user",{method:"PUT",body:JSON.stringify($)}),oaa:{...X.oaa,pushes:()=>r5(X,"/oaa/pushes"),clear:()=>r5(X,"/oaa/pushes",{method:"DELETE"})}}}async function Z$(X,$="local-ecv-token"){let Q=X.replace(/\/$/,""),K=await fetch(`${Q}/__ecv/info`,{headers:{Authorization:`Bearer ${$}`}});if(!K.ok){let G=await K.text();throw Error(`ECV control API GET /info failed: ${K.status} ${G}`)}let Y=await K.json(),Z=new URL(Q);return x4({...Y,baseUrl:Q,host:Z.hostname,port:Number(Z.port||(Z.protocol==="https:"?443:80))})}var s5=3515;class X${container;ecv;constructor(X,$){this.container=X,this.ecv=$}get handle(){return this.ecv}serialize(){let{reset:X,setTestUser:$,oaa:Q,...K}=this.ecv,{pushes:Y,clear:Z,...G}=Q;return{...K,oaa:G}}async stop(){await this.container.stop()}}class e5{options;constructor(X={}){this.options=X}async createContainer(){if(this.options.dockerfileContext)return _4.fromDockerfile(this.options.dockerfileContext,this.options.dockerfileName??"packages/ecv-server/Dockerfile").build(this.options.image??"ionite/ecv:local",{deleteOnExit:!0});return new _4(this.options.image??process.env.ECV_IMAGE??`quay.io/ionite/ecv:${Q1}`)}async start(){let X=await this.createContainer();X.withExposedPorts(s5).withEnvironment({HOST:"0.0.0.0",PORT:String(s5),...this.options.redisUrl?{REDIS_URL:this.options.redisUrl}:{}}).withWaitStrategy(V7.forHttp("/readyz",s5)).withStartupTimeout(this.options.startupTimeout??120000);let $=await X.start(),Q=`http://${$.getHost()}:${$.getMappedPort(s5)}`,K=await Z$(Q);return new X$($,K)}}import{GenericContainer as R4,Wait as v4}from"testcontainers";async function N7(X={}){let $,Q,K;try{let Y=X.redisUrl;if(!Y)$=await new R4(X.redisImage??"redis:7-alpine").withExposedPorts(6379).withWaitStrategy(v4.forLogMessage(/Ready to accept connections/)).start(),Y=`redis://${$.getHost()}:${$.getMappedPort(6379)}`;let Z=X.postgres?.url;if(X.postgres&&!Z){let G=X.postgres;Q=await new R4(G.image??"postgres:16-alpine").withEnvironment({POSTGRES_DB:G.database,POSTGRES_USER:G.user,POSTGRES_PASSWORD:G.password}).withExposedPorts(5432).withWaitStrategy(v4.forLogMessage(/database system is ready to accept connections/)).start(),Z=`postgres://${encodeURIComponent(G.user)}:${encodeURIComponent(G.password)}@${Q.getHost()}:${Q.getMappedPort(5432)}/${encodeURIComponent(G.database)}`}return K=await new e5({...X.ecv,redisUrl:Y}).start(),{ecv:K.serialize(),redisUrl:Y,databaseUrl:Z,async stop(){await Promise.allSettled([K?.stop(),Q?.stop(),$?.stop()].filter(Boolean))}}}catch(Y){throw await Promise.allSettled([K?.stop(),Q?.stop(),$?.stop()].filter(Boolean)),Y}}import*as b1 from"node:crypto";import{defaultLogger as E4}from"@ionite/core";import{CACHE_BOUNDS as G1,clearJwksCache as Q2,resolveCacheConfig as z$,sso as h4,verifyIdToken as U4}from"@ionite/core/server";import{createSessionReadCache as b4,InMemorySessionStore as U1}from"@ionite/server";import{createValidators as C4}from"@ionite/valibot";var K2="https://idp.ecv.local/realms/ecv",Y2="ecv-oidc-client",j$="ion.sso";function G$(X){let{publicKey:$,privateKey:Q}=b1.generateKeyPairSync("rsa",{modulusLength:2048}),K=$.export({format:"jwk"});return{privateKey:Q,kid:X,jwk:{kty:K.kty,n:K.n,e:K.e,kid:X,use:"sig",alg:"RS256"}}}function W$(X){return Buffer.from(X).toString("base64url")}function H$(X,$){let Q={alg:"RS256",typ:"JWT",kid:X.kid},K=`${W$(JSON.stringify(Q))}.${W$(JSON.stringify($))}`,Y=b1.createSign("RSA-SHA256");return Y.update(K),`${K}.${W$(Y.sign(X.privateKey))}`}function $5(X={}){let $=Math.floor(Date.now()/1000);return{iss:K2,aud:Y2,sub:"ecv-user",iat:$,exp:$+3600,...X}}function y4(){return`https://jwks.ecv.local/${b1.randomUUID()}`}function s1(X){let $={get:0,update:0,delete:0};return{store:{async create(K){await X.create(K)},async get(K){return $.get++,X.get(K)},async update(K,Y){$.update++,await X.update(K,Y)},async delete(K){$.delete++,await X.delete(K)}},counts:$}}function e1(X){let $=new Date(Date.now()+3600000);return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"opaque-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",expires:new Date(Date.now()+3600000).toISOString()},claims:$5({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:$,createdAt:new Date}}function $2(X){return new Request("https://app.ecv.local/",{headers:{cookie:`${j$}.session=${X}`}})}function J$(X){let $=C4(),Q=h4($.sso,E4,void 0,{sessionStore:X,cookiesPrefix:j$},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!Q)throw Error("sso() returned undefined");return Q}function O7(X,$,Q){let K=C4(),Y=h4(K.sso,E4,void 0,{authority:K2,tokenUrl:$,authorizationUrl:"https://idp.ecv.local/authorize",clientId:Y2,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Q,sessionStore:X,cookiesPrefix:j$,cookiesSecure:!1},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!Y)throw Error("sso() returned undefined");return Y}function M7(X){return{sid:X,sub:"ecv-user",authType:"sso",tokens:{access_token:"old-access",id_token:"dummy.not.verified",token_type:"Bearer",scope:"openid",refresh_token:"old-refresh",expires:new Date(Date.now()-1000).toISOString()},claims:$5({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}async function T7(X,$,Q){let K=globalThis.fetch,Y=0;Q2(X),globalThis.fetch=async(Z,G)=>{if((typeof Z==="string"?Z:Z.toString())===X)return Y++,new Response(JSON.stringify({keys:$()}),{status:200,headers:{"Content-Type":"application/json"}});return K(Z,G)};try{return await Q(()=>Y)}finally{globalThis.fetch=K,Q2(X)}}function a(X,$){if(!X)throw Error($)}function t0(X,$,Q){if(X!==$)throw Error(`${Q} (expected ${$}, got ${X})`)}var x7=(X)=>new Promise(($)=>setTimeout($,X));function A4(){return{tests:[{name:"SSO read path builds user from stored claims with NO JWKS fetch",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-read"));let K=J$($),Y=globalThis.fetch,Z=0;globalThis.fetch=async(G,W)=>{return Z++,Y(G,W)};try{let G=await K.resolveUser($2("sid-read"));a(G.status==="authenticated",`expected authenticated, got ${G.status}`),a(G.value.id==="ecv-user",`expected id ecv-user, got ${String(G.value.id)}`),a(Z===0,`read path performed ${Z} fetch(es); expected 0`),t0(Q.get,1,`expected 1 store.get, got ${Q.get}`)}finally{globalThis.fetch=Y}}},{name:"authenticated SSO read path performs ZERO session-store writes (§7.5)",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-nowrite"));let K=J$($);for(let Y=0;Y<3;Y++){let Z=await K.resolveUser($2("sid-nowrite"));a(Z.status==="authenticated",`expected authenticated, got ${Z.status}`)}t0(Q.update,0,`read path must not write (got ${Q.update} update(s))`),t0(Q.delete,0,`read path must not delete a live session (got ${Q.delete})`)}},{name:"SSO refresh rotates the sid, strips the old refresh_token, and never 503s (§1.2)",fn:async()=>{let $=G$("kid-rotate"),Q=y4(),K="https://idp.ecv.local/token",Y=new U1,Z=[],G={async create(H){Z.push(H.sid),await Y.create(H)},get:(H)=>Y.get(H),update:(H,J)=>Y.update(H,J),delete:(H)=>Y.delete(H)};await G.create(M7("sid-old"));let W=globalThis.fetch;Q2(Q),globalThis.fetch=async(H,J)=>{let j=typeof H==="string"?H:H.toString();if(j===Q)return new Response(JSON.stringify({keys:[$.jwk]}),{status:200,headers:{"Content-Type":"application/json"}});if(j==="https://idp.ecv.local/token"){let D=Math.floor(Date.now()/1000),P=H$($,$5({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com",iat:D}));return new Response(JSON.stringify({access_token:"new-access",id_token:P,token_type:"Bearer",refresh_token:"new-refresh",expires_in:3600,refresh_expires_in:86400,scope:"openid"}),{status:200,headers:{"Content-Type":"application/json"}})}return W(H,J)};try{let J=await O7(G,"https://idp.ecv.local/token",Q).resolveUser($2("sid-old"));a(J.status==="authenticated",`an expired token with a valid refresh token must refresh, never 503 — got ${J.status}`);let j=Z.find((q)=>q!=="sid-old");a(!!j,"refresh must rotate to a new sid (new record created)");let D=await G.get(j);a(D?.tokens.access_token==="new-access","the rotated record holds the refreshed access token"),a(D?.tokens.refresh_token==="new-refresh","the rotated record holds the new refresh token");let P=await G.get("sid-old");a(P!=null,"old record survives during the rotation grace window"),a(P?.tokens.refresh_token===void 0,"old record must have its refresh_token stripped during grace");let B=new Date(P?.expiresAt).getTime();a(B<=Date.now()+60000+2000,`old record window must shrink to the grace (<=~60s), got ${B-Date.now()}ms`)}finally{globalThis.fetch=W,Q2(Q)}}},{name:"resolveUser is memoized per Request object (single store round-trip)",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-memo"));let K=J$($),Y=$2("sid-memo");await Promise.all([K.resolveUser(Y),K.resolveUser(Y),K.resolveUser(Y)]),t0(Q.get,1,`same-request resolveUser should hit the store once, got ${Q.get}`)}},{name:"session read cache: repeat get is served locally; miss is not cached; update/delete invalidate",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-cache"));let K=b4($,{ttlMs:30000,maxEntries:1024});await K.get("sid-cache"),await K.get("sid-cache"),t0(Q.get,1,`second get within TTL should be local, underlying got ${Q.get}`),await K.get("missing"),await K.get("missing"),t0(Q.get,3,`misses must not be cached (expected 3 underlying gets, got ${Q.get})`),await K.update("sid-cache",{expiresAt:new Date(Date.now()+3600000)}),await K.get("sid-cache"),t0(Q.get,4,`get after update should re-read (expected 4, got ${Q.get})`),await K.delete("sid-cache");let Y=await K.get("sid-cache");a(Y===null,"session should be gone after delete"),t0(Q.get,5,`get after delete should re-read (expected 5, got ${Q.get})`)}},{name:"session read cache: entry expires after ttlMs and re-reads",fn:async()=>{let{store:$,counts:Q}=s1(new U1);await $.create(e1("sid-ttl"));let K=b4($,{ttlMs:20,maxEntries:1024});await K.get("sid-ttl"),t0(Q.get,1,`first get reaches store, got ${Q.get}`),await x7(35),await K.get("sid-ttl"),t0(Q.get,2,`get after TTL expiry should re-read (expected 2, got ${Q.get})`)}},{name:"RemoteConfig.cache values are clamped, not honored, when out of range",fn:async()=>{let $=z$({jwks:{ttlSeconds:1,maxEntries:1},session:{read:{enabled:!0,ttlSeconds:0,maxEntries:1}}});a($.jwks.ttlMs===G1.jwksTtlSeconds.min*1000,`jwks.ttl should clamp to min, got ${$.jwks.ttlMs}`),a($.jwks.maxEntries===G1.jwksMaxEntries.min,"jwks.maxEntries should clamp to min"),a($.session.read.ttlMs===G1.sessionReadTtlSeconds.min*1000,"session.read.ttl should clamp to min"),a($.session.read.maxEntries===G1.sessionReadMaxEntries.min,"read.maxEntries clamp to min");let Q=z$({jwks:{ttlSeconds:1e7,maxEntries:1e6},session:{read:{enabled:!0,ttlSeconds:1e4,maxEntries:1e7}}});a(Q.jwks.ttlMs===G1.jwksTtlSeconds.max*1000,"jwks.ttl should clamp to max"),a(Q.jwks.maxEntries===G1.jwksMaxEntries.max,"jwks.maxEntries should clamp to max"),a(Q.session.read.ttlMs===G1.sessionReadTtlSeconds.max*1000,"session.read.ttl should clamp to max");let K=z$(void 0);a(K.jwks.ttlMs===G1.jwksTtlSeconds.default*1000,"jwks.ttl default"),a(K.session.read.enabled===!1,"read cache must default to disabled")}},{name:"JWKS/CryptoKey caching: repeated verifications = one fetch; verifies after key rotation",fn:async()=>{let $=G$("kid-perf-1"),Q=G$("kid-perf-2"),K=y4(),Y=[$.jwk];await T7(K,()=>Y,async(Z)=>{for(let W=0;W<3;W++){let H=await U4({idToken:H$($,$5()),jwksUri:K,issuer:K2,audience:Y2});a(H.sub==="ecv-user","repeated verify should resolve claims")}a(Z()===1,`3 verifications should share one JWKS fetch, got ${Z()}`),Y=[$.jwk,Q.jwk];let G=await U4({idToken:H$(Q,$5()),jwksUri:K,issuer:K2,audience:Y2});a(G.sub==="ecv-user","rotated-key token must verify after refresh"),a(Z()===2,`rotation should trigger exactly one refresh, got ${Z()}`)})}}]}}async function _7(){let X=performance.now(),{tests:$}=A4(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Performance tuning",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{isStoreConflictError as w$,silentLogger as c}from"@ionite/core";import{chunkedMget as R7,createEvalshaRunner as v7,createReactiveRedis as S4,DEFAULT_LIST_LIMIT as U7,DEFAULT_MGET_CHUNK_SIZE as b7,encodeKeyId as y7,MAX_LIST_LIMIT as k4,RedisAccessAssignmentStore as E7,RedisAccessCatalogStore as I4,RedisAccessIndex as g0,RedisMagicLinkStore as m4,RedisSessionStore as Q5,RedisUserStore as K5,scriptSha1 as D$,supportsRunScript as Y5}from"@ionite/redis-core";var I="ecv:redis:";function G0(){let X=new Map,$=new Map,Q=new Map,K={get:0,set:0,del:0,sadd:0,srem:0,smembers:0,mget:0,runScript:0},Y={raw:X,sets:$,ttls:Q,mgetCalls:0,commands:K,roundTrips:0,resetCounts(){Y.mgetCalls=0,Y.roundTrips=0;for(let Z of Object.keys(K))K[Z]=0},async get(Z){return K.get+=1,Y.roundTrips+=1,X.has(Z)?X.get(Z):null},async set(Z,G){K.set+=1,Y.roundTrips+=1,X.set(Z,G),Q.delete(Z)},async setex(Z,G,W){Y.roundTrips+=1,X.set(Z,W),Q.set(Z,Date.now()+G*1000)},async del(Z){K.del+=1,Y.roundTrips+=1,X.delete(Z),$.delete(Z),Q.delete(Z)},async ttl(Z){if(!X.has(Z))return-2;let G=Q.get(Z);if(G==null)return-1;return Math.max(0,Math.ceil((G-Date.now())/1000))},async getdel(Z){Y.roundTrips+=1;let G=X.has(Z)?X.get(Z):null;return X.delete(Z),Q.delete(Z),G},async sadd(Z,...G){K.sadd+=1,Y.roundTrips+=1;let W=$.get(Z)??new Set;for(let H of G)W.add(H);$.set(Z,W)},async srem(Z,...G){K.srem+=1,Y.roundTrips+=1;let W=$.get(Z);if(W)for(let H of G)W.delete(H)},async smembers(Z){return K.smembers+=1,Y.roundTrips+=1,Array.from($.get(Z)??[])},async mget(Z){return K.mget+=1,Y.mgetCalls+=1,Y.roundTrips+=1,Z.map((G)=>X.has(G)?X.get(G):null)}};return Y}function Z5(){let X=G0(),$=(W)=>X.sets.get(W)?.size??0,Q=(W,H)=>{let J=X.sets.get(W)??new Set;J.add(H),X.sets.set(W,J)},K=(W,H)=>{X.sets.get(W)?.delete(H)},Y=(W)=>Array.from(X.sets.get(W)??[]),Z=(W)=>{X.raw.set(W,"1")},G=(W)=>{X.raw.delete(W),X.sets.delete(W)};return X.runScript=async(W,H,J)=>{if(X.commands.runScript+=1,X.roundTrips+=1,W.includes("local C=tonumber(ARGV[2])")){let j=J[0],D=Number(J[1]),P=Number(J[2]),B=Number(J[3]),q=H[0],L=H[1];for(let F=0;F<D;F+=1){let T=2+F*4;G(H[T]),G(H[T+1]),G(H[T+2]),G(H[T+3])}let O=2+D*4;for(let F=0;F<P;F+=1)G(H[O+F]);G(q);let M=O+P;for(let F=0;F<B;F+=1){let T=M+F*5,[x,b,p,F0,k]=[H[T],H[T+1],H[T+2],H[T+3],H[T+4]],C=4+F*3,[X0,u0,J2]=[J[C],J[C+1],J[C+2]];Q(b,X0),Z(p),Q(F0,J2),Z(k),Q(x,u0),Q(q,u0)}if(B===0)K(L,j);else Q(L,j);return"ok"}if(W.includes("local A=tonumber(ARGV[2])")){let j=J[0],D=Number(J[1]),P=Number(J[2]),B=H[0],q=H[1];for(let M=0;M<D;M+=1){let F=2+M*5,[T,x,b,p,F0]=[H[F],H[F+1],H[F+2],H[F+3],H[F+4]],k=3+M*3,[C,X0,u0]=[J[k],J[k+1],J[k+2]];Q(T,j),Z(x),Q(b,X0),Z(p),Q(B,C),Q(F0,C),Q(q,u0)}let L=2+D*5,O=3+D*3;for(let M=0;M<P;M+=1){let F=L+M*5,[T,x,b,p,F0]=[H[F],H[F+1],H[F+2],H[F+3],H[F+4]],k=O+M*2,[C,X0]=[J[k],J[k+1]];if(K(T,j),$(T)===0){if(G(T),G(x),K(F0,C),K(b,X0),$(b)===0)G(b),G(p)}K(B,C)}return"ok"}if(W.includes("local n=tonumber(ARGV[2])")){let j=J[0],D=Number(J[1]),P=H[0];for(let B=0;B<D;B+=1){let q=1+B*5,[L,O,M,F,T]=[H[q],H[q+1],H[q+2],H[q+3],H[q+4]],[x,b]=[J[2+B*2],J[3+B*2]];if(K(L,j),$(L)===0){if(G(L),G(O),K(T,x),K(M,b),$(M)===0)G(M),G(F)}}return G(P),"ok"}if(W.includes("local roots={}")){let j=Number(J[0]),D=H[0],P=[],B=new Set;for(let q=0;q<j;q+=1){let L=1+q*4,[O,M,F,T]=[H[L],H[L+1],H[L+2],H[L+3]];for(let x of Y(O))if(!B.has(x))B.add(x),P.push(x);G(O),G(M),G(F),G(T)}return G(D),P}if(W.includes("for i=1,#KEYS do")){for(let j of H)G(j);return"ok"}throw Error("authz scripted adapter: unsupported script")},X}function X5(){let X=G0();return X.runScript=async($,Q,K)=>{if($.includes("return 'created'")){let Y=Q[0],Z=K[0],G=X.raw.has(Y)?X.raw.get(Y):null;if(G!=null)return G===Z?"exists_identical":"conflict";if(K.length>=2)await X.setex(Y,Number(K[1]),Z);else await X.set(Y,Z);return"created"}if($.includes("return 'retry'")){let Y=Q[0],Z=K[0],G=K[1],W=X.raw.has(Y)?X.raw.get(Y):null;if(W==null)return"missing";if(W!==Z)return"retry";if(K.length>=3)await X.setex(Y,Number(K[2]),G);else{let H=X.ttls.get(Y);if(X.raw.set(Y,G),H!=null)X.ttls.set(Y,H)}return"ok"}if($.includes("return {1,ARGV[1]}")){let[Y,Z]=Q,[G,W]=K,H=X.raw.get(Y);if(H!==void 0)return[0,H];X.raw.set(Y,G);let J=X.sets.get(Z)??new Set;return J.add(W),X.sets.set(Z,J),[1,G]}throw Error("scripted memory adapter: unsupported script")},X}function y1(X,$,Q){return{sid:X,sub:`user-${$}`,user:{id:`user-${$}`},createdAt:new Date(0),...Q?{expiresAt:Q}:{}}}function f4(){return{tests:[{name:"corrupt JSON payloads are discarded (get -> undefined, list skips) instead of throwing (§1.27)",fn:async()=>{let $=G0(),Q=new K5({redis:$,keyPrefix:I,logger:c});await Q.upsert({id:"u-ok",userName:"ok",name:"OK",email:"ok@example.test"}),$.raw.set(`${I}user:${y7("u-bad")}`,"{ this is : not valid json");let K=$.sets.get(`${I}user:ids`);z(K!=null,"user id set should exist after the first upsert"),K?.add("u-bad");let Y=await Q.get("u-bad");w(Y,void 0,"a corrupt record must read back as undefined, not throw");let Z=await Q.get("u-ok");z(Z?.id==="u-ok","a valid record beside a corrupt one must still read");let G=await Q.list();w(G.items.length,1,"list must skip the corrupt record and return only valid ones"),w(G.items[0]?.id,"u-ok","the surviving record should be the valid one")}},{name:"RedisUserStore maintains the optional externalId secondary index",fn:async()=>{let $=new K5({redis:G0(),keyPrefix:I,logger:c});await $.upsert({id:"local-user",externalId:"enterprise-user",userName:"indexed@example.test",name:"Indexed User",email:"indexed@example.test"}),w((await $.getByExternalId("enterprise-user"))?.id,"local-user","externalId resolves the app-owned user id"),await $.upsert({id:"local-user",externalId:"enterprise-user-renamed",userName:"indexed@example.test",name:"Indexed User",email:"indexed@example.test"}),w(await $.getByExternalId("enterprise-user"),void 0,"renaming clears the stale index"),w((await $.getByExternalId("enterprise-user-renamed"))?.id,"local-user","renaming installs the new index")}},{name:"list() bounds the page: omitted limit -> default cap, oversized limit -> max cap (§7.3)",fn:async()=>{let $=G0(),Q=new K5({redis:$,keyPrefix:I,logger:c}),K=k4*2+50;for(let G=0;G<K;G+=1)await Q.upsert({id:`u-${G}`,userName:`u-${G}`,name:`User ${G}`,email:`u${G}@example.test`});let Y=await Q.list();w(Y.total,K,"total must reflect every matching row (before pagination)"),w(Y.items.length,U7,"an omitted limit must default-cap the page");let Z=await Q.list({limit:K});w(Z.items.length,k4,"an oversized explicit limit must clamp to the max page"),w(Z.total,K,"total stays the true count even when the page is clamped")}},{name:"chunkedMget batches a large key set (no unbounded MGET) and preserves order + nulls (§7.3)",fn:async()=>{let $=Array.from({length:5000},(Z,G)=>`k${G}`),Q=0,K=0,Y=await R7($,async(Z)=>{return K+=1,Q=Math.max(Q,Z.length),Z.map((G)=>Number(G.slice(1))%2===0?G:null)});z(K>1,"a 5000-key MGET must be split into multiple bounded batches"),z(Q<=b7,`each MGET batch must stay within the chunk size (saw ${Q})`),w(Y.length,$.length,"chunked result length must match input length"),w(Y[0],"k0","order must be preserved across chunks (first even key)"),w(Y[1],null,"missing values must normalize to null (first odd key)"),w(Y[4998],"k4998","last even key preserved"),w(Y[4999],null,"last odd key normalizes to null")}},{name:"create-if-absent: identical replay succeeds; a different value is a typed conflict (§5.9)",fn:async()=>{let $=G0(),Q=new Q5({redis:$,keyPrefix:I,logger:c}),K=y1("s1","a");await Q.create(K),await Q.create(y1("s1","a"));let Y;try{await Q.create(y1("s1","b"))}catch(J){Y=J}z(w$(Y),"a different value at the same sid must throw a typed StoreConflictError");let Z=new m4({redis:$,keyPrefix:I,logger:c}),G={id:"c1",userName:"c1"},W=new Date(Date.now()+60000);await Z.create("tok",G,W);let H;try{await Z.create("tok",{id:"c2",userName:"c2"},W)}catch(J){H=J}z(w$(H),"a magic-link token collision must throw a typed StoreConflictError")}},{name:"unique secondary index: a second id claiming an in-use userName conflicts and writes nothing (§5.9)",fn:async()=>{let $=G0(),Q=new K5({redis:$,keyPrefix:I,logger:c});await Q.upsert({id:"u1",userName:"alice",name:"Alice",email:"a@example.test"}),await Q.upsert({id:"u1",userName:"alice",name:"Alice II",email:"a@example.test"});let K;try{await Q.upsert({id:"u2",userName:"alice",name:"Mallory",email:"m@example.test"})}catch(Y){K=Y}z(w$(K),"claiming an in-use userName must throw a typed conflict"),w(await Q.get("u2"),void 0,"a conflicting upsert must not persist the record (checks-first)"),w((await Q.get("u1"))?.name,"Alice II","the original owner record is untouched by the conflict")}},{name:"session update merges fields (no clobber) and a missing session throws (§5.9)",fn:async()=>{let $=G0(),Q=new Q5({redis:$,keyPrefix:I,logger:c});await Q.create(y1("s1","a"));let K=new Date(Date.now()+1e6);await Q.update("s1",{expiresAt:K,tenantId:"tnt-1"});let Y=await Q.get("s1");w(new Date(Y?.expiresAt).getTime(),K.getTime(),"the patched field is applied"),w(Y?.createdAt?.getTime(),0,"unpatched fields are preserved (shallow merge, no clobber)");let Z=!1;try{await Q.update("missing",{tenantId:"x"})}catch{Z=!0}z(Z,"updating a missing session must throw")}},{name:"session create derives a bounded TTL from expiresAt; a rotation-grace update shrinks it (§1.3)",fn:async()=>{let $=X5(),Q=new Q5({redis:$,keyPrefix:I,logger:c});await Q.create(y1("s-ttl","ttl",new Date(Date.now()+1e6)));let K=Array.from($.raw.keys()).find((W)=>W.includes("session"));z(!!K,"a session key must exist after create");let Y=await $.ttl?.(K)??-1;z(Y>0&&Y<=1000,`create must set a bounded TTL from expiresAt (got ${Y})`),await Q.update("s-ttl",{tenantId:"tnt"});let Z=await $.ttl?.(K)??-1;z(Z>0,`an update without expiresAt must KEEPTTL (got ${Z})`),await Q.update("s-ttl",{expiresAt:new Date(Date.now()+60000)});let G=await $.ttl?.(K)??-1;z(G>0&&G<=60,`grace re-TTL must shrink the key TTL to the grace (got ${G})`)}},{name:"session create with no runScript (fallback path) still applies a TTL via setex (§1.3)",fn:async()=>{let $=G0();await new Q5({redis:$,keyPrefix:I,logger:c}).create(y1("s-ttl-fb","fb",new Date(Date.now()+500000)));let K=Array.from($.raw.keys()).find((Z)=>Z.includes("session"));z(!!K,"a session key must exist after fallback create");let Y=await $.ttl?.(K)??-1;z(Y>0&&Y<=500,`fallback create must setex a bounded TTL (got ${Y})`)}},{name:"hasAll/hasAny evaluate many targets with a single batched MGET (§7.4)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c}),K=[{subject:"u1",subjectType:"user",targetType:"permission",target:"p0"}];await Q.materialize("root1",K);let Y=Array.from({length:50},(Z,G)=>({targetType:"permission",target:`p${G}`,resource:`r${G}`}));$.mgetCalls=0,await Q.hasAll("u1",Y,"user"),w($.mgetCalls,1,"hasAll over 50 targets must issue exactly one batched MGET"),$.mgetCalls=0,await Q.hasAny("u1",Y,"user"),w($.mgetCalls,1,"hasAny over 50 targets must issue exactly one batched MGET")}},{name:"access list total stays accurate after revoke (§7.5)",fn:async()=>{let $=G0(),Q=new E7({redis:$,keyPrefix:I,logger:c});await Q.grant({subject:"u1",subjectType:"user",targetType:"role",target:"r1"}),await Q.grant({subject:"u1",subjectType:"user",targetType:"role",target:"r2"});let K=await Q.list();w(K.total,2,"both active grants are counted"),await Q.revoke({subject:"u1",subjectType:"user",targetType:"role",target:"r1"});let Y=await Q.list();w(Y.total,1,"revoke removes the row from the id set so the total is accurate"),w(Y.items.length,1,"only the surviving grant is returned"),w(Y.items[0]?.target,"r2","the surviving grant is the non-revoked one")}},{name:"list() returns a stable deterministic order with disjoint, complete offset pages",fn:async()=>{let $=G0(),Q=new K5({redis:$,keyPrefix:I,logger:c}),K=["u3","u1","u5","u2","u4"];for(let J of K)await Q.upsert({id:J,userName:`${J}@example.com`});let Y=await Q.list({start:0,limit:2}),Z=await Q.list({start:2,limit:2}),G=await Q.list({start:4,limit:2});w(Y.total,5,"total reflects the full set on every page");let W=[...Y.items,...Z.items,...G.items].map((J)=>J.id);w(W.join(","),"u1,u2,u3,u4,u5","pages are disjoint, complete, and in stable ascending id order");let H=await Q.list({start:0,limit:2});w(H.items.map((J)=>J.id).join(","),"u1,u2","the same offset window is byte-stable across repeated list() calls")}},{name:"reactive adapter close() before first readiness rejects ready() instead of hanging (§5.8)",fn:async()=>{let Q=S4({source:{async subscribe(){return()=>{}}},path:"redis/main",createClient:async()=>({adapter:G0()})});await Q.close();let K=!1;try{await Q.ready(50)}catch{K=!0}z(K,"ready() must reject promptly after close() before first readiness, not hang")}},{name:"reactive adapter forwards runScript + getdel from the active client (§10.1)",fn:async()=>{let $=X5(),K=S4({source:{async subscribe(Y,Z){return Z({value:{},metadata:{version:1}}),()=>{}}},path:"redis/main",createClient:async()=>({adapter:$})});await K.ready(1000),z(Y5(K),"reactive adapter must forward runScript once the client supports it"),z(typeof K.getdel==="function","reactive adapter must forward getdel once the client supports it"),await K.set(`${I}fwd:k`,"v"),w(await K.getdel?.(`${I}fwd:k`),"v","forwarded getdel returns and deletes the value"),w(await K.get(`${I}fwd:k`),null,"forwarded getdel deleted the key"),await K.close()}},{name:"single-use consume over runScript/getdel yields exactly one winner (§10.1)",fn:async()=>{let $=X5();z(Y5($),"scripted adapter must advertise runScript for this test");let Q=new m4({redis:$,keyPrefix:I,logger:c}),K={id:"c1",userName:"c1"};await Q.create("one-time",K,new Date(Date.now()+60000));let Z=(await Promise.all(Array.from({length:8},()=>Q.getAndDelete("one-time")))).filter((G)=>G!=null);w(Z.length,1,"exactly one concurrent consume may win the single-use token")}},{name:"session update over the runScript CAS path preserves arrays and large ints (§8.2)",fn:async()=>{let $=X5();z(Y5($),"scripted adapter must advertise runScript for this test");let Q=new Q5({redis:$,keyPrefix:I,logger:c}),K=Number.MAX_SAFE_INTEGER,Y={sid:"s-merge",sub:"user-merge",user:{id:"user-merge"},claims:{groups:[],seq:K},createdAt:new Date(0),expiresAt:new Date(Date.now()+1e6)};await Q.create(Y),await Q.update("s-merge",{tenantId:"tnt-merge"});let Z=await Q.get("s-merge");z(Z!=null,"session must exist after update"),w(Z?.tenantId,"tnt-merge","the patched field is applied"),z(Array.isArray(Z?.claims.groups),"an empty-array claim must stay an array (not become {})"),w((Z?.claims.groups).length,0,"the empty-array claim stays empty"),w(Z?.claims.seq,K,"a large-integer claim survives the merge unchanged")}},{name:"key encoding: injected `:` cannot alias another edge in the access index (§10.2)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-a",[{subject:"u",subjectType:"user",targetType:"permission",target:"p",resource:"x:y"}]),z((await Q.has("u","permission","p","x:y","user")).allow,"the materialized edge (target p, resource x:y) must be allowed"),z(!(await Q.has("u","permission","p:x","y","user")).allow,"a colon-shifted edge must NOT alias the real edge — it must be denied")}},{name:"key encoding: subject-disabled marker is keyed to the exact id, not a `:`-prefix (§10.2)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.setSubjectDisabled("team:a","user",!0),z(await Q.isSubjectDisabled("team:a","user"),"the disabled marker round-trips for a `:`-bearing id"),z(!await Q.isSubjectDisabled("team","user"),"a prefix subject id must not alias another subject’s disabled marker"),z(!await Q.isSubjectDisabled("team:a","customer"),"the veto is namespaced by subjectType")}},{name:"RedisAccessCatalogStore insertIfAbsent is atomic and concurrent callers converge on one row",fn:async()=>{let $=X5(),Q=new I4({redis:$,keyPrefix:I,logger:c}),K=Q.groups.insertIfAbsent({id:"code-admin",name:"Code Admin",description:"Code presentation",source:"code",roles:["admin"]}),Y=Q.groups.insertIfAbsent({id:"code-admin",name:"IAM Renamed Admin",description:"IAM-owned presentation",source:"code",providerId:"provider-42",requestUrl:"https://iam.example.test/request/provider-42",roles:["must-not-win"]});await Promise.all([K.ready(),Y.ready()]);let Z=await Q.groups.get("code-admin");z(Z!==void 0,"one durable catalog row is installed"),w(K.name,Z?.name,"the first caller receives the winning row"),w(Y.name,Z?.name,"the racing caller converges on the same winning row"),w((await Q.groups.list()).items.length,1,"concurrent insertIfAbsent calls never duplicate the id-set row")}},{name:"RedisAccessCatalogStore suppresses reactive emission for a value-identical set",fn:async()=>{let $=new I4({redis:G0(),keyPrefix:I,logger:c}),Q=0,K=$.afterChange?.(()=>{Q+=1});await $.groups.set({id:"stable-group",name:"Stable Group",description:"No-op write proof",requestable:!0,metadata:{first:1,second:2}}).ready(),await $.groups.set({requestable:!0,description:"No-op write proof",name:"Stable Group",id:"stable-group",metadata:{second:2,first:1}}).ready(),K?.(),w(Q,1,"the durable no-op write emits no second reactive change")}},{name:"key encoding: a pathologically long id produces a bounded key (§10.2)",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c}),K="x".repeat(1e4);await Q.materialize("root-huge",[{subject:K,subjectType:"user",targetType:"permission",target:K}]);for(let Y of $.raw.keys())z(Y.length<512,`key must stay bounded even for a 10k-char id (saw ${Y.length})`),z(!Y.includes(K),"the raw oversized id must never appear verbatim in a key");for(let Y of $.sets.keys())z(Y.length<512,`set key must stay bounded even for a 10k-char id (saw ${Y.length})`)}},{name:"Lua teardown: removeByRoot of a 1000-edge root is O(1) client round trips, not O(edges) (§7.2)",fn:async()=>{let $=Z5();z(Y5($),"scripted authz adapter must advertise runScript for the Lua path");let Q=new g0({redis:$,keyPrefix:I,logger:c}),K=Array.from({length:1000},(Y,Z)=>({subject:"big",subjectType:"user",targetType:"permission",target:`p${Z}`}));await Q.materialize("root-big",K),z((await Q.hasPermission("big","p0",void 0,"user")).allow,"a materialized edge resolves before teardown"),z((await Q.hasPermission("big","p999",void 0,"user")).allow,"the last materialized edge resolves too"),$.resetCounts(),await Q.removeByRoot("root-big"),w($.commands.smembers,1,"teardown reads the root descriptor set with exactly one SMEMBERS"),w($.commands.runScript,1,"teardown applies every edge removal in exactly one runScript"),z($.roundTrips<=3,`a 1000-edge teardown must be O(1) client round trips, not O(edges) (saw ${$.roundTrips})`),z(!(await Q.hasPermission("big","p0",void 0,"user")).allow,"edges are gone after teardown"),z(!(await Q.hasPermission("big","p999",void 0,"user")).allow,"the last edge is gone after teardown")}},{name:"diff-materialize: re-materializing identical edges is a no-op (no write, no §5.3 deny window)",fn:async()=>{let $=Z5(),Q=new g0({redis:$,keyPrefix:I,logger:c}),K=[{subject:"u",subjectType:"user",targetType:"role",target:"a"},{subject:"u",subjectType:"user",targetType:"role",target:"b"}];await Q.materialize("root-x",K),$.resetCounts(),await Q.materialize("root-x",K),w($.commands.runScript,0,"an empty diff must not invoke the write script at all"),z((await Q.hasRole("u","a",void 0,"user")).allow,"edge a stays present across a no-op re-materialize"),z((await Q.hasRole("u","b",void 0,"user")).allow,"edge b stays present across a no-op re-materialize")}},{name:"replaceSubject atomically installs a complete Redis closure in one script",fn:async()=>{let $=Z5(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-old",[{subject:"swap",subjectType:"user",targetType:"permission",target:"old",rootId:"root-old"}]),$.resetCounts(),await Q.replaceSubject("swap","user",[{subject:"swap",subjectType:"user",targetType:"permission",target:"new",rootId:"root-new"},{subject:"swap",subjectType:"user",targetType:"permission",target:"shared",rootId:"root-new"},{subject:"swap",subjectType:"user",targetType:"permission",target:"shared",rootId:"root-second"}]),w($.commands.runScript,1,"the complete closure replacement uses exactly one atomic script"),z(!(await Q.hasPermission("swap","old",void 0,"user")).allow,"the old closure is removed at the swap"),z((await Q.hasPermission("swap","new",void 0,"user")).allow,"the new closure is installed"),z((await Q.hasPermission("swap","shared",void 0,"user")).allow,"overlapping roots preserve their shared edge"),await Q.removeByRoot("root-new"),z((await Q.hasPermission("swap","shared",void 0,"user")).allow,"the second root still justifies the shared edge")}},{name:"replaceSubject fails before mutation when a Redis adapter lacks atomic scripts",fn:async()=>{let $=G0(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-old",[{subject:"safe",subjectType:"user",targetType:"permission",target:"old",rootId:"root-old"}]);let K=!1;try{await Q.replaceSubject("safe","user",[])}catch{K=!0}z(K,"replaceSubject requires runScript instead of exposing a non-atomic fallback"),z((await Q.hasPermission("safe","old",void 0,"user")).allow,"last-known-good access survives the rejected replace")}},{name:"diff-materialize: removing one edge leaves the unchanged sibling continuously present (§5.3)",fn:async()=>{let $=Z5(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.materialize("root-y",[{subject:"u",subjectType:"user",targetType:"role",target:"keep"},{subject:"u",subjectType:"user",targetType:"role",target:"drop"}]),await Q.materialize("root-y",[{subject:"u",subjectType:"user",targetType:"role",target:"keep"}]),z((await Q.hasRole("u","keep",void 0,"user")).allow,"the unchanged edge survives a sibling removal"),z(!(await Q.hasRole("u","drop",void 0,"user")).allow,"the removed edge is gone")}},{name:"teardown never touches idx:disabled: a deactivated subject stays vetoed after removeByRoot (§7.2)",fn:async()=>{let $=Z5(),Q=new g0({redis:$,keyPrefix:I,logger:c});await Q.setSubjectDisabled("u","user",!0),await Q.materialize("root-z",[{subject:"u",subjectType:"user",targetType:"role",target:"a"}]),await Q.removeByRoot("root-z"),z(await Q.isSubjectDisabled("u","user"),"the subject-disabled marker is independent of edge materialization and survives teardown")}},{name:"fallback teardown (no runScript): removeByRoot issues O(edges) commands, not ~5×edges",fn:async()=>{let $=G0();z(!Y5($),"this test intends to exercise the no-runScript fallback");let Q=new g0({redis:$,keyPrefix:I,logger:c}),K=Array.from({length:100},(Y,Z)=>({subject:"f",subjectType:"user",targetType:"permission",target:`p${Z}`}));await Q.materialize("root-fb",K),$.resetCounts(),await Q.removeByRoot("root-fb"),z($.roundTrips<=1200,`fallback teardown must be O(edges) with a small constant (saw ${$.roundTrips})`),z($.roundTrips<1e4,`fallback teardown must not be quadratic in edges (saw ${$.roundTrips})`),z(!(await Q.hasPermission("f","p0",void 0,"user")).allow,"fallback teardown removes edges"),z(!(await Q.hasPermission("f","p99",void 0,"user")).allow,"fallback teardown removes the last edge")}},{name:"EVALSHA memoization: stable script body → one sha, EVALSHA hits, NOSCRIPT falls back to EVAL (§7.8)",fn:async()=>{let $=0,Q=0,K=new Set,Z=v7({async eval(H,J,j){return $+=1,K.add(D$(H)),"ok"},async evalsha(H,J,j){if(Q+=1,!K.has(H))throw Error("NOSCRIPT No matching script. Please use EVAL.");return"ok"}}),G=`redis.call('SET', KEYS[1], ARGV[1])
2
2
  return 'ok'`;await Z(G,["k"],["v1"]),w(Q,1,"the runner always tries EVALSHA first"),w($,1,"a cold NOSCRIPT falls back to EVAL exactly once"),await Z(G,["k"],["v2"]),await Z(G,["k"],["v3"]),w($,1,"a stable script body never re-transmits the source after the first prime"),w(Q,3,"steady-state calls all go over EVALSHA (the 40-byte sha)");let W=`redis.call('DEL', KEYS[1])
3
- return 'ok'`;z(D9(G)!==D9(W),"distinct script bodies hash to distinct shas")}}]}}async function h7(){let X=performance.now(),{tests:$}=f4(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Redis stores",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{RedisTenantStore as z2}from"@ionite/redis-core";import{InMemoryAccessCatalogStore as C7,InMemoryTenantStore as Z5,ionite as A7,silentLogger as p4}from"@ionite/server";import{createValidators as S7}from"@ionite/valibot";import{LRUCache as X5}from"lru-cache";function k7(X){return{async load(){return X},async subscribe(){return},secret:{}}}function q1(X,$=X){return{id:X,companyId:`co-${X}`,companyName:$,environmentType:"DEV",status:"completed",configLocator:{type:"vault",vaultPath:`secret/data/tenants/${X}`},createdAt:new Date,updatedAt:new Date}}function z5(X,$){return{close:$,__marker:X}}function F1(X){return X?.__marker}function g4(){let X=new Map,$=new Map;return{async get(Q){return X.has(Q)?X.get(Q):null},async set(Q,K){X.set(Q,K)},async del(Q){X.delete(Q)},async mget(Q){return Q.map((K)=>X.has(K)?X.get(K):null)},async sadd(Q,...K){let Y=$.get(Q)??new Set;for(let Z of K)Y.add(Z);return $.set(Q,Y),K.length},async srem(Q,...K){let Y=$.get(Q);if(!Y)return 0;let Z=0;for(let G of K)if(Y.delete(G))Z+=1;return Z},async smembers(Q){return Array.from($.get(Q)??[])}}}function u4(){return{tests:[{name:"getIon() without an ionCache throws a guidance error — the SDK never constructs runtimes (§7.4)",fn:async()=>{let $=new Z5;await $.upsert(q1("t1"));let Q;try{await $.getIon("t1")}catch(K){Q=K.message}z(Q!==void 0,"getIon() must throw when no ionCache is configured"),z(/ionCache/.test(Q??""),"the error must point at the required ionCache pattern")}},{name:"the app cache single-flights concurrent construction (two getIon for one id => one build) (§7.4)",fn:async()=>{let $={constructions:0,closeCount:0},Q,K=new X5({max:50,dispose:(G)=>G.close(),fetchMethod:async(G)=>{let W=await Q.get(G);if(!W)return;return await Promise.resolve(),$.constructions+=1,z5(W.companyName,()=>{$.closeCount+=1})}});Q=new Z5({ionCache:K}),await Q.upsert(q1("t1"));let[Y,Z]=await Promise.all([Q.getIon("t1"),Q.getIon("t1")]);w($.constructions,1,"concurrent getIon must coalesce into a single construction"),z(Y===Z,"both callers must receive the same cached instance")}},{name:"upsert invalidates and closes the cached runtime; the next getIon constructs fresh (§7.4)",fn:async()=>{let $={constructions:0,closeCount:0},Q,K=new X5({max:50,dispose:(G)=>G.close(),fetchMethod:async(G)=>{let W=await Q.get(G);if(!W)return;return $.constructions+=1,z5(W.companyName,()=>{$.closeCount+=1})}});Q=new Z5({ionCache:K}),await Q.upsert(q1("t1","v1"));let Y=await Q.getIon("t1");w(F1(Y),"v1","first build reflects the v1 record"),w($.constructions,1,"one construction after the first getIon"),await Q.upsert(q1("t1","v2")),w($.closeCount,1,"upsert must close the previously cached runtime");let Z=await Q.getIon("t1");w(F1(Z),"v2","the next getIon rebuilds from the updated record"),w($.constructions,2,"a fresh construction follows invalidation")}},{name:"delete closes the cached runtime (§7.4)",fn:async()=>{let $={constructions:0,closeCount:0},Q,K=new X5({max:50,dispose:(Y)=>Y.close(),fetchMethod:async(Y)=>{let Z=await Q.get(Y);if(!Z)return;return $.constructions+=1,z5(Z.companyName,()=>{$.closeCount+=1})}});Q=new Z5({ionCache:K}),await Q.upsert(q1("t1")),await Q.getIon("t1"),w($.closeCount,0,"no close before deletion"),await Q.delete("t1"),w($.closeCount,1,"delete must close the cached runtime")}},{name:"ion.close() detaches the authz catalog-store listener; a closed instance stops reacting (§6.1)",fn:async()=>{let $=new C7,Q=0,K=0,Y=$.afterChange.bind($);$.afterChange=(H)=>{Q+=1;let J=Y((j)=>{return K+=1,H(j)});return()=>{Q-=1,J()}};let Z=(H)=>A7(k7({id:H}),{validators:S7(),stores:{catalogStore:$}}),G=Z("batch6-authz-a"),W=Z("batch6-authz-b");await G.ready(1000),await W.ready(1000),w(Q,2,"both instances subscribe exactly one catalog listener"),K=0,$.groups.set({id:"g1"}),w(K,2,"both instances react to the change while open"),G.close(),w(Q,1,"ion.close() detaches the closed instance’s catalog listener"),K=0,$.groups.set({id:"g2"}),w(K,1,"exactly one reconcile/projection fires after the other instance is closed"),W.close(),w(Q,0,"closing the last instance leaves no listeners on the shared store")}},{name:"cross-node staleness is TTL-bounded: upsert on node A does not invalidate node B (§3.1)",fn:async()=>{let $=g4(),Q=`ion:t:batch6-node:${crypto.randomUUID()}:`,K=()=>{let G,W=new X5({max:50,dispose:(H)=>H.close(),fetchMethod:async(H)=>{let J=await G.get(H);if(!J)return;return z5(J.companyName,()=>{})}});return G=new z2({redis:$,keyPrefix:Q,logger:p4,ionCache:W}),{store:G,ionCache:W}},Y=K(),Z=K();await Y.store.upsert(q1("t","v1")),w(F1(await Y.store.getIon("t")),"v1","node A builds from v1"),w(F1(await Z.store.getIon("t")),"v1","node B builds from v1"),await Y.store.upsert(q1("t","v2")),w(F1(await Y.store.getIon("t")),"v2","node A rebuilds from the new record"),w(F1(await Z.store.getIon("t")),"v1","node B keeps serving the old runtime until its own cache evicts (documented staleness)"),Z.ionCache.delete("t"),w(F1(await Z.store.getIon("t")),"v2","after eviction node B rebuilds from the shared record")}},{name:'the Redis key-prefix "constructed twice" warning fires at most once per fingerprint (§6.2)',fn:async()=>{let $=g4(),Q=[],K={...p4,warn:(W)=>{Q.push(String(W))}},Y=`ion:t:batch6-dup:${crypto.randomUUID()}:`,Z={redis:$,keyPrefix:Y,logger:K};new z2(Z),new z2(Z),new z2(Z),new z2(Z);let G=Q.filter((W)=>W.includes(Y));w(G.length,1,"the duplicate-prefix warning must fire at most once per fingerprint")}}]}}async function I7(){let X=performance.now(),{tests:$}=u4(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Tenant lifecycle",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{assertEcid as l4,EcidFormatError as B9,formatEcid as d4,ION_SCIM_USER_EXTENSION_URN as c4,isEcid as n4,parseEcid as i4}from"@ionite/core";import{createControlValidators as m7,createValidators as f7,BaseUser as p7}from"@ionite/valibot";import{createControlValidators as g7,createValidators as u7,BaseUser as l7}from"@ionite/zod";var s4=u7(),e4=f7(),d7=g7(),c7=m7();async function o4(X,$){return!(await X["~standard"].validate($)).issues}async function S0(X,$,Q,K,Y){let Z=await o4(X,Q),G=await o4($,Q);z(Z===G,`${Y}: zod and valibot disagree (zod ${Z?"accepted":"rejected"}, valibot ${G?"accepted":"rejected"})`),z(Z===K,`${Y}: expected both packages to ${K?"accept":"reject"}`)}function G5(){return{z:s4.sso.idTokenClaims,v:e4.sso.idTokenClaims}}function W5(){return{z:s4.iam.user,v:e4.iam.user}}function a4(){return{z:l7,v:p7}}var P9="ecid:acme.com:01J9F3Z8QK7M2N4P6R8T0V2W4X",q9=["not-an-ecid","ecid:acme.com","ecid::opaque","ecid:acme.com:","urn:x:acme:1","ecid:acme com:abc"],t4={userName:"u@example.com",name:"U",email:"u@example.com"},r4={userName:"u@example.com"};function F9(){return{z:d7.sso.registerSSOAppPayload,v:c7.sso.registerSSOAppPayload}}var H5={sub:"user-1",iss:"https://idp.example/realm",aud:"client-1",exp:2000000000,iat:1};function $8(){return{tests:[{name:"§10.1 ID-token claims: complete claims accepted (both packages)",fn:async()=>{let{z:$,v:Q}=G5();await S0($,Q,H5,!0,"valid id-token claims")}},{name:"§10.1 ID-token claims: array aud accepted (both packages)",fn:async()=>{let{z:$,v:Q}=G5();await S0($,Q,{...H5,aud:["client-1","api://resource"]},!0,"array aud")}},{name:"§10.1 ID-token claims: missing sub/iss/aud/exp each rejected (both packages)",fn:async()=>{let{z:$,v:Q}=G5();for(let K of["sub","iss","aud","exp"]){let Y={...H5};delete Y[K],await S0($,Q,Y,!1,`id-token missing ${K}`)}}},{name:"§10.1 ID-token claims: empty aud array rejected (both packages)",fn:async()=>{let{z:$,v:Q}=G5();await S0($,Q,{...H5,aud:[]},!1,"empty aud array")}},{name:"§10.2 SCIM user: a present password is rejected (both packages)",fn:async()=>{let{z:$,v:Q}=W5();await S0($,Q,{userName:"u@example.com",password:"hunter2"},!1,"scim user with password")}},{name:"§10.2 SCIM user: no password accepted (both packages)",fn:async()=>{let{z:$,v:Q}=W5();await S0($,Q,{userName:"u@example.com"},!0,"scim user without password")}},{name:"§10.2 registerSSOApp: javascript:/data: and non-URL redirectUris rejected (both packages)",fn:async()=>{let{z:$,v:Q}=F9(),K=["javascript:alert(1)","data:text/html,<script>alert(1)</script>","not-a-url","ftp://example.com/cb","http://evil.example.com/cb",`java
4
- script:alert(1)`];for(let Y of K)await S0($,Q,{redirectUris:[Y]},!1,`redirectUri ${JSON.stringify(Y)}`)}},{name:"§10.2 registerSSOApp: https and http://localhost redirectUris accepted (both packages)",fn:async()=>{let{z:$,v:Q}=F9(),K=["https://app.example.com/auth/callback","http://localhost:3000/cb","http://127.0.0.1:3000/cb"];for(let Y of K)await S0($,Q,{redirectUris:[Y]},!0,`redirectUri ${JSON.stringify(Y)}`)}},{name:"§10.2 registerSSOApp: empty redirectUris array rejected (both packages)",fn:async()=>{let{z:$,v:Q}=F9();await S0($,Q,{redirectUris:[]},!1,"empty redirectUris")}},{name:"ECID: formatEcid/parseEcid/isEcid round-trip a valid ECID",fn:async()=>{let $=d4("acme.com","01J9F3Z8QK7M2N4P6R8T0V2W4X");z($===P9,`formatEcid produced ${$}`),z(n4($),"isEcid should accept a formatted ECID");let Q=i4($);z(Q.realm==="acme.com"&&Q.opaque==="01J9F3Z8QK7M2N4P6R8T0V2W4X","parseEcid segments"),z(l4($)===$,"assertEcid returns the value")}},{name:"ECID: malformed values are rejected by isEcid and throw from parse/assert/format",fn:async()=>{for(let Q of q9){z(!n4(Q),`isEcid should reject ${JSON.stringify(Q)}`);let K=!1;try{i4(Q)}catch(Z){K=Z instanceof B9}z(K,`parseEcid should throw EcidFormatError for ${JSON.stringify(Q)}`);let Y=!1;try{l4(Q)}catch(Z){Y=Z instanceof B9}z(Y,`assertEcid should throw EcidFormatError for ${JSON.stringify(Q)}`)}let $=!1;try{d4("acme com","abc")}catch(Q){$=Q instanceof B9}z($,"formatEcid should throw EcidFormatError for an invalid realm")}},{name:"ECID: SCIM user with a valid ionite extension ecid/accountClass accepted (both packages)",fn:async()=>{let{z:$,v:Q}=W5(),K={...r4,[c4]:{ecid:P9,accountClass:"privileged"}};await S0($,Q,K,!0,"scim user with valid ionite extension")}},{name:"ECID: SCIM user with a malformed ecid rejected (both packages)",fn:async()=>{let{z:$,v:Q}=W5();for(let K of q9){let Y={...r4,[c4]:{ecid:K}};await S0($,Q,Y,!1,`scim user with malformed ecid ${JSON.stringify(K)}`)}}},{name:"ECID: BaseUser with a valid ecid accepted (both packages)",fn:async()=>{let{z:$,v:Q}=a4();await S0($,Q,{...t4,ecid:P9,accountClass:"daily"},!0,"base user valid ecid")}},{name:"ECID: BaseUser with a malformed ecid rejected (both packages)",fn:async()=>{let{z:$,v:Q}=a4();for(let K of q9)await S0($,Q,{...t4,ecid:K},!1,`base user malformed ecid ${JSON.stringify(K)}`)}}]}}async function n7(){let X=performance.now(),{tests:$}=$8(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Validator parity",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}function i3(X){let{describe:$,it:Q,beforeAll:K}=X.hooks,Y,Z,G,W,H=new Set,J=new Set,j={baseUrl:()=>{if(!Y)throw Error("ECV setup has not completed");return Y.baseUrl},ecv:()=>{if(!Y)throw Error("ECV setup has not completed");return Y.ecv},basePath:()=>{let q=X.basePath??Y?.basePath;if(!q)throw Error("ECV basePath must be provided in ecvSuite options or returned by setup()");return q},capabilitiezPath:X.capabilitiezPath,timeout:X.timeout??15000,skipTlsVerify:X.skipTlsVerify,headers:X.headers,ion:X.ion};function D(){return W??=(async()=>{Y=await X.setup();try{Z=await e5(j)}catch(q){G=q}})(),W}function P(q,L=!0){let O=new Map;for(let M of q){let F=O.get(M.suite)??[];F.push(M),O.set(M.suite,F)}for(let[M,F]of O)$(M,()=>{K(D);for(let T of F)Q(T.title,async(x)=>{if(L&&!Z){let b=G instanceof Error?G.message:String(G);throw Error(`capabilitiez manifest unavailable: ${b}`)}await T.run(Z,x)})})}function B(q,L){if(J.has(q))throw Error(`ECV unit ${q} was registered more than once`);J.add(q),H.add(q),P(Y9(j,q,L))}return P($9(()=>[...H])),{sso:{login:(q)=>B("sso.login",q),jit:()=>B("sso.jit"),logout:()=>B("sso.logout"),back_channel_logout:()=>B("sso.back_channel_logout")},ciam:{customer:()=>B("ciam.customer"),magic_link:()=>B("ciam.magic_link"),oidc_login:()=>B("ciam.oidc_login"),logout:()=>B("ciam.logout")},iam:{users:()=>B("iam.users"),groups:()=>B("iam.groups")},workload:{incoming:()=>B("workload.incoming"),outgoing:()=>B("workload.outgoing")},authz:{oaa:()=>B("authz.oaa")},tenants:(q)=>{if(J.has("tenant.management"))throw Error("ECV tenant management tests were registered more than once");J.add("tenant.management"),P(K9(j,q),!1)},sdk:()=>{if(J.has("sdk"))throw Error("ECV SDK tests were registered more than once");J.add("sdk"),P(Q9(),!1)}}}function o3(X){console.log(`
3
+ return 'ok'`;z(D$(G)!==D$(W),"distinct script bodies hash to distinct shas")}}]}}async function h7(){let X=performance.now(),{tests:$}=f4(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Redis stores",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{RedisTenantStore as z5}from"@ionite/redis-core";import{InMemoryAccessCatalogStore as C7,InMemoryTenantStore as Z2,ionite as A7,silentLogger as p4}from"@ionite/server";import{createValidators as S7}from"@ionite/valibot";import{LRUCache as X2}from"lru-cache";function k7(X){return{async load(){return X},async subscribe(){return},secret:{}}}function q1(X,$=X){return{id:X,companyId:`co-${X}`,companyName:$,environmentType:"DEV",status:"completed",configLocator:{type:"vault",vaultPath:`secret/data/tenants/${X}`},createdAt:new Date,updatedAt:new Date}}function z2(X,$){return{close:$,__marker:X}}function F1(X){return X?.__marker}function g4(){let X=new Map,$=new Map;return{async get(Q){return X.has(Q)?X.get(Q):null},async set(Q,K){X.set(Q,K)},async del(Q){X.delete(Q)},async mget(Q){return Q.map((K)=>X.has(K)?X.get(K):null)},async sadd(Q,...K){let Y=$.get(Q)??new Set;for(let Z of K)Y.add(Z);return $.set(Q,Y),K.length},async srem(Q,...K){let Y=$.get(Q);if(!Y)return 0;let Z=0;for(let G of K)if(Y.delete(G))Z+=1;return Z},async smembers(Q){return Array.from($.get(Q)??[])}}}function u4(){return{tests:[{name:"getIon() without an ionCache throws a guidance error — the SDK never constructs runtimes (§7.4)",fn:async()=>{let $=new Z2;await $.upsert(q1("t1"));let Q;try{await $.getIon("t1")}catch(K){Q=K.message}z(Q!==void 0,"getIon() must throw when no ionCache is configured"),z(/ionCache/.test(Q??""),"the error must point at the required ionCache pattern")}},{name:"the app cache single-flights concurrent construction (two getIon for one id => one build) (§7.4)",fn:async()=>{let $={constructions:0,closeCount:0},Q,K=new X2({max:50,dispose:(G)=>G.close(),fetchMethod:async(G)=>{let W=await Q.get(G);if(!W)return;return await Promise.resolve(),$.constructions+=1,z2(W.companyName,()=>{$.closeCount+=1})}});Q=new Z2({ionCache:K}),await Q.upsert(q1("t1"));let[Y,Z]=await Promise.all([Q.getIon("t1"),Q.getIon("t1")]);w($.constructions,1,"concurrent getIon must coalesce into a single construction"),z(Y===Z,"both callers must receive the same cached instance")}},{name:"upsert invalidates and closes the cached runtime; the next getIon constructs fresh (§7.4)",fn:async()=>{let $={constructions:0,closeCount:0},Q,K=new X2({max:50,dispose:(G)=>G.close(),fetchMethod:async(G)=>{let W=await Q.get(G);if(!W)return;return $.constructions+=1,z2(W.companyName,()=>{$.closeCount+=1})}});Q=new Z2({ionCache:K}),await Q.upsert(q1("t1","v1"));let Y=await Q.getIon("t1");w(F1(Y),"v1","first build reflects the v1 record"),w($.constructions,1,"one construction after the first getIon"),await Q.upsert(q1("t1","v2")),w($.closeCount,1,"upsert must close the previously cached runtime");let Z=await Q.getIon("t1");w(F1(Z),"v2","the next getIon rebuilds from the updated record"),w($.constructions,2,"a fresh construction follows invalidation")}},{name:"delete closes the cached runtime (§7.4)",fn:async()=>{let $={constructions:0,closeCount:0},Q,K=new X2({max:50,dispose:(Y)=>Y.close(),fetchMethod:async(Y)=>{let Z=await Q.get(Y);if(!Z)return;return $.constructions+=1,z2(Z.companyName,()=>{$.closeCount+=1})}});Q=new Z2({ionCache:K}),await Q.upsert(q1("t1")),await Q.getIon("t1"),w($.closeCount,0,"no close before deletion"),await Q.delete("t1"),w($.closeCount,1,"delete must close the cached runtime")}},{name:"ion.close() detaches the authz catalog-store listener; a closed instance stops reacting (§6.1)",fn:async()=>{let $=new C7,Q=0,K=0,Y=$.afterChange.bind($);$.afterChange=(H)=>{Q+=1;let J=Y((j)=>{return K+=1,H(j)});return()=>{Q-=1,J()}};let Z=(H)=>A7(k7({id:H}),{validators:S7(),stores:{catalogStore:$}}),G=Z("batch6-authz-a"),W=Z("batch6-authz-b");await G.ready(1000),await W.ready(1000),w(Q,2,"both instances subscribe exactly one catalog listener"),K=0,$.groups.set({id:"g1"}),w(K,2,"both instances react to the change while open"),G.close(),w(Q,1,"ion.close() detaches the closed instance’s catalog listener"),K=0,$.groups.set({id:"g2"}),w(K,1,"exactly one reconcile/projection fires after the other instance is closed"),W.close(),w(Q,0,"closing the last instance leaves no listeners on the shared store")}},{name:"cross-node staleness is TTL-bounded: upsert on node A does not invalidate node B (§3.1)",fn:async()=>{let $=g4(),Q=`ion:t:batch6-node:${crypto.randomUUID()}:`,K=()=>{let G,W=new X2({max:50,dispose:(H)=>H.close(),fetchMethod:async(H)=>{let J=await G.get(H);if(!J)return;return z2(J.companyName,()=>{})}});return G=new z5({redis:$,keyPrefix:Q,logger:p4,ionCache:W}),{store:G,ionCache:W}},Y=K(),Z=K();await Y.store.upsert(q1("t","v1")),w(F1(await Y.store.getIon("t")),"v1","node A builds from v1"),w(F1(await Z.store.getIon("t")),"v1","node B builds from v1"),await Y.store.upsert(q1("t","v2")),w(F1(await Y.store.getIon("t")),"v2","node A rebuilds from the new record"),w(F1(await Z.store.getIon("t")),"v1","node B keeps serving the old runtime until its own cache evicts (documented staleness)"),Z.ionCache.delete("t"),w(F1(await Z.store.getIon("t")),"v2","after eviction node B rebuilds from the shared record")}},{name:'the Redis key-prefix "constructed twice" warning fires at most once per fingerprint (§6.2)',fn:async()=>{let $=g4(),Q=[],K={...p4,warn:(W)=>{Q.push(String(W))}},Y=`ion:t:batch6-dup:${crypto.randomUUID()}:`,Z={redis:$,keyPrefix:Y,logger:K};new z5(Z),new z5(Z),new z5(Z),new z5(Z);let G=Q.filter((W)=>W.includes(Y));w(G.length,1,"the duplicate-prefix warning must fire at most once per fingerprint")}}]}}async function I7(){let X=performance.now(),{tests:$}=u4(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Tenant lifecycle",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}import{assertEcid as l4,EcidFormatError as B$,formatEcid as d4,ION_SCIM_USER_EXTENSION_URN as c4,isEcid as n4,parseEcid as i4}from"@ionite/core";import{createControlValidators as m7,createValidators as f7,BaseUser as p7}from"@ionite/valibot";import{createControlValidators as g7,createValidators as u7,BaseUser as l7}from"@ionite/zod";var s4=u7(),e4=f7(),d7=g7(),c7=m7();async function o4(X,$){return!(await X["~standard"].validate($)).issues}async function S0(X,$,Q,K,Y){let Z=await o4(X,Q),G=await o4($,Q);z(Z===G,`${Y}: zod and valibot disagree (zod ${Z?"accepted":"rejected"}, valibot ${G?"accepted":"rejected"})`),z(Z===K,`${Y}: expected both packages to ${K?"accept":"reject"}`)}function G2(){return{z:s4.sso.idTokenClaims,v:e4.sso.idTokenClaims}}function W2(){return{z:s4.iam.user,v:e4.iam.user}}function a4(){return{z:l7,v:p7}}var P$="ecid:acme.com:01J9F3Z8QK7M2N4P6R8T0V2W4X",q$=["not-an-ecid","ecid:acme.com","ecid::opaque","ecid:acme.com:","urn:x:acme:1","ecid:acme com:abc"],t4={userName:"u@example.com",name:"U",email:"u@example.com"},r4={userName:"u@example.com"};function F$(){return{z:d7.sso.registerSSOAppPayload,v:c7.sso.registerSSOAppPayload}}var H2={sub:"user-1",iss:"https://idp.example/realm",aud:"client-1",exp:2000000000,iat:1};function $8(){return{tests:[{name:"§10.1 ID-token claims: complete claims accepted (both packages)",fn:async()=>{let{z:$,v:Q}=G2();await S0($,Q,H2,!0,"valid id-token claims")}},{name:"§10.1 ID-token claims: array aud accepted (both packages)",fn:async()=>{let{z:$,v:Q}=G2();await S0($,Q,{...H2,aud:["client-1","api://resource"]},!0,"array aud")}},{name:"§10.1 ID-token claims: missing sub/iss/aud/exp each rejected (both packages)",fn:async()=>{let{z:$,v:Q}=G2();for(let K of["sub","iss","aud","exp"]){let Y={...H2};delete Y[K],await S0($,Q,Y,!1,`id-token missing ${K}`)}}},{name:"§10.1 ID-token claims: empty aud array rejected (both packages)",fn:async()=>{let{z:$,v:Q}=G2();await S0($,Q,{...H2,aud:[]},!1,"empty aud array")}},{name:"§10.2 SCIM user: a present password is rejected (both packages)",fn:async()=>{let{z:$,v:Q}=W2();await S0($,Q,{userName:"u@example.com",password:"hunter2"},!1,"scim user with password")}},{name:"§10.2 SCIM user: no password accepted (both packages)",fn:async()=>{let{z:$,v:Q}=W2();await S0($,Q,{userName:"u@example.com"},!0,"scim user without password")}},{name:"§10.2 registerSSOApp: javascript:/data: and non-URL redirectUris rejected (both packages)",fn:async()=>{let{z:$,v:Q}=F$(),K=["javascript:alert(1)","data:text/html,<script>alert(1)</script>","not-a-url","ftp://example.com/cb","http://evil.example.com/cb",`java
4
+ script:alert(1)`];for(let Y of K)await S0($,Q,{redirectUris:[Y]},!1,`redirectUri ${JSON.stringify(Y)}`)}},{name:"§10.2 registerSSOApp: https and http://localhost redirectUris accepted (both packages)",fn:async()=>{let{z:$,v:Q}=F$(),K=["https://app.example.com/auth/callback","http://localhost:3000/cb","http://127.0.0.1:3000/cb"];for(let Y of K)await S0($,Q,{redirectUris:[Y]},!0,`redirectUri ${JSON.stringify(Y)}`)}},{name:"§10.2 registerSSOApp: empty redirectUris array rejected (both packages)",fn:async()=>{let{z:$,v:Q}=F$();await S0($,Q,{redirectUris:[]},!1,"empty redirectUris")}},{name:"ECID: formatEcid/parseEcid/isEcid round-trip a valid ECID",fn:async()=>{let $=d4("acme.com","01J9F3Z8QK7M2N4P6R8T0V2W4X");z($===P$,`formatEcid produced ${$}`),z(n4($),"isEcid should accept a formatted ECID");let Q=i4($);z(Q.realm==="acme.com"&&Q.opaque==="01J9F3Z8QK7M2N4P6R8T0V2W4X","parseEcid segments"),z(l4($)===$,"assertEcid returns the value")}},{name:"ECID: malformed values are rejected by isEcid and throw from parse/assert/format",fn:async()=>{for(let Q of q$){z(!n4(Q),`isEcid should reject ${JSON.stringify(Q)}`);let K=!1;try{i4(Q)}catch(Z){K=Z instanceof B$}z(K,`parseEcid should throw EcidFormatError for ${JSON.stringify(Q)}`);let Y=!1;try{l4(Q)}catch(Z){Y=Z instanceof B$}z(Y,`assertEcid should throw EcidFormatError for ${JSON.stringify(Q)}`)}let $=!1;try{d4("acme com","abc")}catch(Q){$=Q instanceof B$}z($,"formatEcid should throw EcidFormatError for an invalid realm")}},{name:"ECID: SCIM user with a valid ionite extension ecid/accountClass accepted (both packages)",fn:async()=>{let{z:$,v:Q}=W2(),K={...r4,[c4]:{ecid:P$,accountClass:"privileged"}};await S0($,Q,K,!0,"scim user with valid ionite extension")}},{name:"ECID: SCIM user with a malformed ecid rejected (both packages)",fn:async()=>{let{z:$,v:Q}=W2();for(let K of q$){let Y={...r4,[c4]:{ecid:K}};await S0($,Q,Y,!1,`scim user with malformed ecid ${JSON.stringify(K)}`)}}},{name:"ECID: BaseUser with a valid ecid accepted (both packages)",fn:async()=>{let{z:$,v:Q}=a4();await S0($,Q,{...t4,ecid:P$,accountClass:"daily"},!0,"base user valid ecid")}},{name:"ECID: BaseUser with a malformed ecid rejected (both packages)",fn:async()=>{let{z:$,v:Q}=a4();for(let K of q$)await S0($,Q,{...t4,ecid:K},!1,`base user malformed ecid ${JSON.stringify(K)}`)}}]}}async function n7(){let X=performance.now(),{tests:$}=$8(),Q=[];for(let G of $)Q.push(await N(G.name,async()=>{await G.fn();return}));let K=performance.now()-X,Y=Q.filter((G)=>!G.passed).length,Z=Q.filter((G)=>G.details?.skipped).length;return{suite:"Validator parity",passed:Y===0,tests:Q,duration:K,summary:{total:Q.length,passed:Q.length-Y-Z,failed:Y,skipped:Z}}}function i3(X){let{describe:$,it:Q,beforeAll:K}=X.hooks,Y,Z,G,W,H=new Set,J=new Set,j={baseUrl:()=>{if(!Y)throw Error("ECV setup has not completed");return Y.baseUrl},ecv:()=>{if(!Y)throw Error("ECV setup has not completed");return Y.ecv},basePath:()=>{let q=X.basePath??Y?.basePath;if(!q)throw Error("ECV basePath must be provided in ecvSuite options or returned by setup()");return q},capabilitiezPath:X.capabilitiezPath,timeout:X.timeout??15000,skipTlsVerify:X.skipTlsVerify,headers:X.headers,ion:X.ion};function D(){return W??=(async()=>{Y=await X.setup();try{Z=await e2(j)}catch(q){G=q}})(),W}function P(q,L=!0){let O=new Map;for(let M of q){let F=O.get(M.suite)??[];F.push(M),O.set(M.suite,F)}for(let[M,F]of O)$(M,()=>{K(D);for(let T of F)Q(T.title,async(x)=>{if(L&&!Z){let b=G instanceof Error?G.message:String(G);throw Error(`capabilitiez manifest unavailable: ${b}`)}await T.run(Z,x)})})}function B(q,L){if(J.has(q))throw Error(`ECV unit ${q} was registered more than once`);J.add(q),H.add(q),P(Y$(j,q,L))}return P($$(()=>[...H])),{sso:{login:(q)=>B("sso.login",q),jit:()=>B("sso.jit"),logout:()=>B("sso.logout"),back_channel_logout:()=>B("sso.back_channel_logout")},ciam:{customer:()=>B("ciam.customer"),magic_link:()=>B("ciam.magic_link"),oidc_login:()=>B("ciam.oidc_login"),logout:()=>B("ciam.logout")},iam:{users:()=>B("iam.users"),groups:()=>B("iam.groups")},workload:{incoming:()=>B("workload.incoming"),outgoing:()=>B("workload.outgoing")},authz:{oaa:()=>B("authz.oaa")},tenants:(q)=>{if(J.has("tenant.management"))throw Error("ECV tenant management tests were registered more than once");J.add("tenant.management"),P(K$(j,q),!1)},sdk:()=>{if(J.has("sdk"))throw Error("ECV SDK tests were registered more than once");J.add("sdk"),P(Q$(),!1)}}}function o3(X){console.log(`
5
5
  =`.repeat(60)),console.log("Enterprise Control Validation Report"),console.log("=".repeat(60)),console.log(`Timestamp: ${X.timestamp.toISOString()}`),console.log(`Base URL: ${X.baseUrl}`),console.log(`Duration: ${X.duration.toFixed(2)}ms`),console.log(`Overall: ${X.passed?"PASSED":"FAILED"}`);for(let $ of X.suites){console.log(`
6
6
  ${$.suite} Suite ${$.passed?"PASSED":"FAILED"}`),console.log(` Passed: ${$.summary.passed}/${$.summary.total}`+($.summary.skipped>0?` (${$.summary.skipped} skipped)`:""));for(let Q of $.tests)if(console.log(` ${Q.details?.skipped?"SKIP":Q.passed?"PASS":"FAIL"} ${Q.name}`),Q.error)console.log(` Error: ${Q.error}`)}}
7
- export{E1 as a,W8 as b,T0 as c,r as d,N as e,W1 as f,z as g,w as h,r0 as i,J2 as j,j2 as k,U9 as l,j8 as m,N8 as n,w2 as o,D2 as p,_8 as q,N5 as r,O5 as s,M5 as t,T5 as u,x5 as v,f1 as w,k0 as x,_5 as y,p8 as z,F2 as A,L2 as B,n8 as C,V2 as D,i8 as E,Q1 as F,U5 as G,G$ as H,W$ as I,O2 as J,QQ as K,M2 as L,KQ as M,T2 as N,JQ as O,A5 as P,d1 as Q,PQ as R,x2 as S,_2 as T,NQ as U,b2 as V,TQ as W,C2 as X,CQ as Y,k2 as Z,gQ as _,c1 as $,lQ as aa,H4 as ba,n5 as ca,l2 as da,sQ as ea,n2 as fa,Z7 as ga,G7 as ha,o2 as ia,e5 as ja,$9 as ka,Q9 as la,K9 as ma,Y9 as na,x4 as oa,Z9 as pa,X9 as qa,e2 as ra,N7 as sa,A4 as ta,_7 as ua,f4 as va,h7 as wa,u4 as xa,I7 as ya,$8 as za,n7 as Aa,i3 as Ba,o3 as Ca};
7
+ export{E1 as a,W8 as b,T0 as c,r as d,N as e,W1 as f,z as g,w as h,r0 as i,J5 as j,j5 as k,U$ as l,j8 as m,N8 as n,w5 as o,D5 as p,_8 as q,N2 as r,O2 as s,M2 as t,T2 as u,x2 as v,f1 as w,k0 as x,_2 as y,p8 as z,F5 as A,L5 as B,n8 as C,V5 as D,i8 as E,Q1 as F,U2 as G,G9 as H,W9 as I,O5 as J,QQ as K,M5 as L,KQ as M,T5 as N,JQ as O,A2 as P,d1 as Q,PQ as R,x5 as S,_5 as T,NQ as U,b5 as V,TQ as W,C5 as X,CQ as Y,k5 as Z,gQ as _,c1 as $,lQ as aa,H4 as ba,n2 as ca,l5 as da,sQ as ea,n5 as fa,Z7 as ga,G7 as ha,o5 as ia,e2 as ja,$$ as ka,Q$ as la,K$ as ma,Y$ as na,x4 as oa,Z$ as pa,X$ as qa,e5 as ra,N7 as sa,A4 as ta,_7 as ua,f4 as va,h7 as wa,u4 as xa,I7 as ya,$8 as za,n7 as Aa,i3 as Ba,o3 as Ca};