@ionite/ecv 0.0.24-beta.20261006.21 → 0.0.24-beta.20261007.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/ecv-f128wc0g.js +9 -0
- package/dist/vitest.js +1 -1
- package/package.json +7 -7
- package/dist/shared/ecv-aap0yrpk.js +0 -9
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{resolveReadyPath as Xa,startAppUnderTest as ie,waitForReady as ce}from"@ionite/devtools";import{ACCEPT_JSON as zd,CONTENT_TYPE_FORM as Dd,CONTENT_TYPE_SCIM as Nt}from"@ionite/core";import{AssignmentPolicyError as Un,accessRefId as zr,accessRootId as ys,assertRequiredSubjectType as Vd,CatalogAdjacencyCache as Dr,deepEqualPlain as On,defineCatalog as ma,escapeScimFilterValue as Za,expandRoot as Ms,hasAllScope as qo,hasAnyScope as Jr,hasPermission as gs,hasScope as $o,InMemoryAccessAssignmentStore as it,InMemoryAccessCatalogStore as St,InMemoryAccessIndex as lt,InMemoryResourceStore as Mo,InMemoryUserStore as Gr,ION_AUTHZ_GROUP_SCHEMA as Pt,ION_AUTHZ_OAA_SYNC as jo,ION_IAM_GROUPS_WRITE as Gt,ION_IAM_READ as Qt,ION_IAM_USERS_WRITE as Wr,ION_TENANT_MANAGE as sn,ionite as ot,readJsonResponse as Bd,stableConfigHash as xa,unionCatalogRead as Vr}from"@ionite/server";import{createValidators as rt}from"@ionite/valibot";function n(Be){return typeof Be.baseUrl==="function"?Be.baseUrl():Be.baseUrl}function Ra(Be,Ne){let Le=typeof Be==="function"?Be():Be;if(!Le)throw Error(`${Ne} is required`);return Le}function a(Be){let Ne=Be.timeout??5000;return async function(ze,De={}){let Ve=n(Be),Je=ze.startsWith("http://")||ze.startsWith("https://")?ze:new URL(ze,Ve).href,Ge=new AbortController,We=setTimeout(()=>Ge.abort(),Ne),Ke=typeof Be.headers==="function"?await Be.headers():Be.headers??{};try{return await fetch(Je,{...De,signal:Ge.signal,headers:{...Ke,...De.headers},redirect:"manual"})}finally{clearTimeout(We)}}}async function s(Be,Ne){let Le=performance.now();try{let ze=await Ne();return{name:Be,passed:!0,duration:performance.now()-Le,details:ze?.details}}catch(ze){return{name:Be,passed:!1,error:ze instanceof Error?ze.message:String(ze),duration:performance.now()-Le}}}function c(Be,Ne){return{name:Be,passed:!0,duration:0,details:{skipped:!0,reason:Ne}}}function e(Be,Ne){if(!Be)throw Error(Ne)}function t(Be,Ne,Le){if(Be!==Ne)throw Error(Le??`Expected ${JSON.stringify(Ne)}, got ${JSON.stringify(Be)}`)}function r(Be,Ne,Le){let ze=Ne["~standard"].validate(Be);if(ze instanceof Promise)throw Error(Le??"Async validators are not supported in assertValid. Use the validator directly with await.");if("issues"in ze){let Ve=ze.issues.map((Je)=>{let Ge=Je.path?Je.path.map(String).join("."):"";return Ge?`${Ge}: ${Je.message}`:Je.message});throw Error(Le??`Validation failed: ${Ve.join("; ")}`)}}function qs(Be){let Ne=new Map,Le=Be.getSetCookie?.()??[];for(let ze of Le){let[De]=ze.split(";"),[Ve,Je]=De.split("=");if(Ve&&Je!==void 0)Ne.set(Ve.trim(),Je.trim())}return Ne}function oa(Be){return Array.from(Be.entries()).map(([Ne,Le])=>`${Ne}=${Le}`).join("; ")}function mt(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}async function wt(){let Be=new St;await Be.permissions.set({id:"read",name:"Read",description:"Read",source:"app",assignableSubjectTypes:["user"]}).ready(),await Be.permissions.set({id:"write",name:"Write",description:"Write",source:"app",assignableSubjectTypes:["user"]}).ready(),await Be.permissions.set({id:"manage",name:"Manage",description:"Manage (cascades to sub-resources)",applyToSubResources:!0,source:"app",assignableSubjectTypes:["user"]}).ready(),await Be.roles.set({id:"editor",name:"Editor",description:"Editor",permissions:["read","write"],source:"app",assignableSubjectTypes:["user"]}).ready(),await Be.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app",assignableSubjectTypes:["user"]}).ready(),await Be.roles.set({id:"ops",name:"Ops",description:"Ops",permissions:["manage"],source:"app",assignableSubjectTypes:["user"]}).ready(),await Be.groups.set({id:"staff",providerId:"iam-staff",name:"Staff",description:"Staff",requestable:!0,assignableSubjectTypes:["user"],roles:["editor"],source:"app"}).ready(),await Be.groups.set({id:"internal_staff",providerId:"iam-internal-staff",name:"Internal Staff",description:"Internal composition group",assignableSubjectTypes:["user"],roles:["editor"],source:"app"}).ready();let Ne=new Mo;return Ne.set({id:"project-a",type:"project",name:"Project A",description:"A",resources:["module-1"]}),Ne.set({id:"module-1",type:"module",name:"Module 1",description:"M1",parent:"project-a"}),Ne.set({id:"region:north",type:"region",name:"North Region",description:"North"}),Ne.set({id:"region:south",type:"region",name:"South Region",description:"South"}),await Be.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:Be,resources:Ne}}function Ws(){return ma({permissions:[{id:"code_read",name:"Code Read",description:"Read (code-declared)",riskLevel:"organization-critical",assignableSubjectTypes:["user"]},{id:"code_write",name:"Code Write",description:"Write (code-declared)",assignableSubjectTypes:["user"]}],roles:[{id:"code_editor",name:"Code Editor",description:"Editor (code-declared)",assignableSubjectTypes:["user"],permissions:["code_read","code_write"]}],groups:[{id:"code_staff",name:"Code Staff",description:"Staff (code-declared)",requestable:!0,assignableSubjectTypes:["user"],roles:["code_editor"],dataSteward:"identity-governance@example.test"},{id:"code_sensitive_reports",name:"Sensitive Reports",description:"Sensitive reports (code-declared)",requestable:!0,riskLevel:"organization-critical",assignableSubjectTypes:["user"],dataSteward:"audit@example.test"}]})}async function kt(Be,Ne,Le,ze){let De={...ze,id:ys(ze),status:"active"};await Be.grant(De);let Ve=await Ms(De,Le);return await Ne.materialize(De.id,Ve),De.id}class qn extends lt{constructor(){super(...arguments);this.failNext=!0}materialize(Be,Ne){if(this.failNext)return this.failNext=!1,Promise.reject(Error("simulated transient index failure"));return super.materialize(Be,Ne)}}class Kr extends lt{constructor(){super(...arguments);this.replacementStarted=new Promise((Be)=>{this.replacementStartedResolve=Be});this.replacementRelease=new Promise((Be)=>{this.replacementReleaseResolve=Be})}async replaceSubject(Be,Ne,Le){this.replacementStartedResolve(),await this.replacementRelease,await super.replaceSubject(Be,Ne,Le)}releaseReplacement(){this.replacementReleaseResolve()}}class Fr extends Gr{upsert(Be){return Promise.reject(Error("simulated user metadata write failure"))}}function u(){return{tests:[{name:"grant a group root materializes the full closure (group -> role -> permissions)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"alice",subjectType:"user",targetType:"group",target:"staff"}),e((await ze.hasGroup("alice","staff",void 0,"user")).allow,"alice should be in group staff"),e((await ze.hasRole("alice","editor",void 0,"user")).allow,"staff confers role editor"),e((await ze.hasPermission("alice","read",void 0,"user")).allow,"editor confers permission read"),e((await ze.hasPermission("alice","write",void 0,"user")).allow,"editor confers permission write"),e(!(await ze.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 Ne=await wt(),Le=new it,ze=new lt,De=await kt(Le,ze,Ne,{subject:"bob",subjectType:"user",targetType:"role",target:"editor"}),Ve=await kt(Le,ze,Ne,{subject:"bob",subjectType:"user",targetType:"role",target:"viewer"});e((await ze.hasPermission("bob","read",void 0,"user")).allow,"read present via both roles"),await Le.revoke({subject:"bob",subjectType:"user",targetType:"role",target:"editor"}),await ze.removeByRoot(De),e((await ze.hasPermission("bob","read",void 0,"user")).allow,"read survives via viewer root"),e(!(await ze.hasPermission("bob","write",void 0,"user")).allow,"write removed with editor root"),await Le.revoke({subject:"bob",subjectType:"user",targetType:"role",target:"viewer"}),await ze.removeByRoot(Ve),e(!(await ze.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 Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"carol",subjectType:"user",targetType:"permission",target:"manage",resources:["project-a"]}),e((await ze.hasPermission("carol","manage","project-a","user")).allow,"manage on scoped resource"),e((await ze.hasPermission("carol","manage","module-1","user")).allow,"manage cascades to sub-resource"),e(!(await ze.hasPermission("carol","manage","other","user")).allow,"manage not on unrelated resource"),e((await ze.hasPermission("carol","manage",void 0,"user")).allow,"manage present for some resource")}},{name:"clear(subject) removes only that subject derived edges",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"dave",subjectType:"user",targetType:"role",target:"viewer"}),await kt(Le,ze,Ne,{subject:"erin",subjectType:"user",targetType:"role",target:"viewer"}),await ze.clear("dave","user"),e(!(await ze.hasPermission("dave","read",void 0,"user")).allow,"dave edges cleared"),e((await ze.hasPermission("erin","read",void 0,"user")).allow,"erin edges retained")}},{name:"AccessAssignmentStore.list returns granted active roots",fn:async()=>{let Ne=new it;await Ne.grant({subject:"frank",subjectType:"user",targetType:"role",target:"viewer",status:"active"}),await Ne.grant({subject:"frank",subjectType:"user",targetType:"role",target:"editor",status:"active"});let Le=await Ne.list();e(Le.items.length===2,"both granted roots should be listed"),e(Le.items.every((ze)=>ze.targetType==="role"),"listed roots are roles")}},{name:"listByTarget returns active members of an entitlement (SCIM member projection source)",fn:async()=>{let Ne=new it;await Ne.grant({subject:"alice",subjectType:"user",targetType:"group",target:"staff",status:"active"}),await Ne.grant({subject:"bob",subjectType:"user",targetType:"group",target:"staff",status:"active"}),await Ne.grant({subject:"carol",subjectType:"user",targetType:"role",target:"viewer",status:"active"});let Le=await Ne.listByTarget("group","staff"),ze=Le.items.map((Ve)=>Ve.subject).sort();e(Le.items.length===2,"both staff members are projected"),e(ze[0]==="alice"&&ze[1]==="bob","staff members are alice and bob"),e(Le.items.every((Ve)=>Ve.target==="staff"),"every projected root targets staff");let De=await Ne.listByTarget("role","viewer");e(De.items.length===1&&De.items[0]?.subject==="carol","viewer has only carol")}},{name:"group delete semantics: revoking each listByTarget root removes membership from the closure",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"grace",subjectType:"user",targetType:"group",target:"staff"}),await kt(Le,ze,Ne,{subject:"heidi",subjectType:"user",targetType:"group",target:"staff"}),e((await ze.hasPermission("grace","read",void 0,"user")).allow,"grace inherits read via staff"),e((await ze.hasPermission("heidi","read",void 0,"user")).allow,"heidi inherits read via staff");let De=await Le.listByTarget("group","staff");for(let Ve of De.items)await Le.revoke(Ve),await ze.removeByRoot(Ve.id??ys(Ve));e(!(await ze.hasGroup("grace","staff",void 0,"user")).allow,"grace no longer in staff"),e(!(await ze.hasPermission("heidi","read",void 0,"user")).allow,"heidi loses inherited read"),e((await Le.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 Ne=new it;await Ne.grant({subject:"ivan",subjectType:"user",targetType:"role",target:"admin",status:"active"}),await Ne.grant({subject:"judy",subjectType:"user",targetType:"permission",target:"admin",status:"active"});let Le=await Ne.listByTarget("role","admin"),ze=await Ne.listByTarget("permission","admin");e(Le.items.length===1&&Le.items[0]?.subject==="ivan","role admin has ivan"),e(ze.items.length===1&&ze.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 Ne=await wt(),Le=new it,ze=new lt,Ve=((await Ne.catalog.groups.get("region_north_librarian"))?.resources??[]).map((Je)=>typeof Je==="string"?Je:Je.id);e(Ve.length===1&&Ve[0]==="region:north","entitlement carries the region binding"),await kt(Le,ze,Ne,{subject:"nina",subjectType:"user",targetType:"group",target:"region_north_librarian",resources:Ve}),e((await ze.hasRole("nina","editor","region:north","user")).allow,"editor scoped to region:north"),e((await ze.hasPermission("nina","read","region:north","user")).allow,"read scoped to region:north"),e((await ze.hasPermission("nina","write","region:north","user")).allow,"write scoped to region:north"),e(!(await ze.hasPermission("nina","read","region:south","user")).allow,"access does not leak to a different region"),e(!(await ze.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:Ne}=await wt(),Le=await Ne.groups.list({filter:{requestable:!0}});e(Le.items.every((De)=>De.requestable===!0),"requestable filter returns only requestable groups"),e(Le.items.some((De)=>De.id==="region_north_librarian"),"the requestable region entitlement is included"),e(!Le.items.some((De)=>De.id==="internal_staff"),"the internal staff group is excluded");let ze=await Ne.groups.list({filter:{requestable:!1}});e(ze.items.some((De)=>De.id==="internal_staff"),"internal_staff (requestable unset) is in the non-requestable set"),e(!ze.items.some((De)=>De.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 Ne=new St,Le=0,ze=Ne.afterChange?.(()=>{Le+=1}),De={id:"stable-group",name:"Stable Group",description:"No-op write proof",requestable:!0,assignableSubjectTypes:["user"],first:1,second:2};await Ne.groups.set(De).ready();let Ve={requestable:!0,description:"No-op write proof",name:"Stable Group",id:"stable-group",assignableSubjectTypes:["user"],second:2,first:1};await Ne.groups.set(Ve).ready(),ze?.(),t(Le,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 Ne=async(Ge,We)=>{let Ke=!1;try{await Ge()}catch{Ke=!0}e(Ke,We)},Le=Ws(),ze=new St;ze.groups.get=async()=>{throw Error("get unavailable")};let De=Vr(Le,ze);e(De!==void 0,"the code/store union is available"),await Ne(()=>De.groups.get("code_staff"),"get failure must propagate");let Ve=new St;Ve.groups.list=async()=>{throw Error("list unavailable")};let Je=Vr(Le,Ve);e(Je!==void 0,"the code/store union is available"),await Ne(()=>Je.groups.list(),"list failure must propagate")}},{name:"ion.authz.oaa becomes ready once OAA config (url/providerId/datasourceId) is applied",fn:async()=>{let Ne=ot(mt({id:"oaa-ready-test",authz:{oaa:{url:"https://veza.example",providerId:"prov",datasourceId:"ds"}}}),{validators:rt()});e(!Ne.authz.oaa.isReady(),"OAA should not be ready before RemoteConfig is applied"),await Ne.ready(1000),e(Ne.authz.oaa.isReady(),"OAA should be ready once url/providerId/datasourceId are present"),await Ne.authz.oaa.ready(1000),e(Ne.readiness().modules.authz.ready===!0,"authz is ready once OAA is configured and applied"),Ne.close?.()}},{name:"ion.authz.oaa stays not ready and sync throws not_configured without OAA config",fn:async()=>{let Ne=ot(mt({id:"no-oaa-test"}),{validators:rt()});await Ne.ready(1000),e(!Ne.authz.oaa.isReady(),"OAA should not be ready when no OAA config is present");let Le=Ne.readiness();e(Le.modules.authz.active===!0,"authz module is always active"),e(Le.modules.authz.ready===!0,"authz is ready when OAA is not configured"),e(Ne.requireReady(["authz"]).ok===!0,"requireReady([authz]) is ok without OAA configured");let ze;try{await Ne.authz.oaa.sync({})}catch(De){ze=De.code}e(ze==="not_configured","sync should throw OaaError with code not_configured when unconfigured"),Ne.close?.()}},{name:"ion.authz.has* delegate to the wired AccessIndex (allow / deny)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"alice",subjectType:"user",targetType:"role",target:"editor"});let De=ot(mt({id:"authz-has-test"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),e((await De.authz.hasRole("alice","editor",void 0,"user")).allow,"ion.authz.hasRole resolves a granted role"),e((await De.authz.hasPermission("alice","read",void 0,"user")).allow,"ion.authz.hasPermission resolves a permission conferred by the role"),e(!(await De.authz.hasPermission("alice","manage",void 0,"user")).allow,"ion.authz.hasPermission denies a permission the subject was never granted"),De.close?.()}},{name:"ion.authz.has* deny by default (not_configured) when no AccessIndex is wired",fn:async()=>{let Ne=ot(mt({id:"authz-noindex-test"}),{validators:rt()});await Ne.ready(1000);let Le=await Ne.authz.hasPermission("alice","read",void 0,"user");e(!Le.allow,"denies when no AccessIndex is configured"),e(Le.metadata?.reason==="access_index_not_configured","decision metadata explains the missing AccessIndex"),Ne.close?.()}},{name:"standalone hasPermission(request, ion) returns unauthenticated -> 401 without a session",fn:async()=>{let Ne=ot(mt({id:"authz-helper-test"}),{validators:rt()});await Ne.ready(1000);let Le=await gs(new Request("https://example.test/"),Ne,"read");e(!Le.allow,"no SSO session -> not allowed"),e(Le.reason==="unauthenticated","reason is unauthenticated when no user is resolved"),e(Le.response().status===401,"response() is 401 for the unauthenticated case"),Ne.close?.()}},{name:"subjectType isolates principals that share an id (user:123 vs customer:123)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"123",subjectType:"user",targetType:"role",target:"editor"}),await kt(Le,ze,Ne,{subject:"123",subjectType:"customer",targetType:"role",target:"viewer"}),e((await ze.hasRole("123","editor",void 0,"user")).allow,"user:123 has editor"),e((await ze.hasPermission("123","write",void 0,"user")).allow,"user:123 has write via editor"),e(!(await ze.hasRole("123","editor",void 0,"customer")).allow,"customer:123 does NOT have editor"),e((await ze.hasRole("123","viewer",void 0,"customer")).allow,"customer:123 has viewer"),e(!(await ze.hasRole("123","viewer",void 0,"user")).allow,"user:123 does NOT have viewer"),e(!(await ze.hasPermission("123","write",void 0,"customer")).allow,"customer:123 has no write"),await ze.clear("123","user"),e(!(await ze.hasRole("123","editor",void 0,"user")).allow,"user:123 edges cleared"),e((await ze.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 Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"cust-1",subjectType:"customer",targetType:"role",target:"viewer"});let De=ot(mt({id:"authz-subjecttype-test"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),e((await De.authz.hasRole("cust-1","viewer",void 0,"customer")).allow,"customer grant resolves under the customer subjectType"),e((await De.authz.hasPermission("cust-1","read",void 0,"customer")).allow,"customer inherits read via viewer"),e(!(await De.authz.hasRole("cust-1","viewer",void 0,"user")).allow,"same id under user subjectType is denied"),e(!(await De.authz.hasRole("cust-1","viewer",void 0,"user")).allow,"same id under explicit user subjectType is denied"),De.close?.()}},{name:"ion.authz.grant writes a customer root through the public chokepoint",fn:async()=>{let Ne=await wt();await Ne.catalog.roles.set({id:"customer_viewer",name:"Customer Viewer",description:"Customer-assignable viewer",permissions:["read"],source:"app",assignableSubjectTypes:["customer"]}).ready();let Le=new it,ze=new lt,De=ot(mt({id:"authz-customer-grant"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});try{await De.ready(1000),await De.authz.grant({subject:"cust-grant",subjectType:"customer",targetType:"role",target:"customer_viewer"}),e((await De.authz.hasRole("cust-grant","customer_viewer",void 0,"customer")).allow,"customer grant resolves under the customer subjectType"),e(!(await De.authz.hasRole("cust-grant","customer_viewer",void 0,"user")).allow,"the same id is invisible to a user check")}finally{De.close?.()}}},{name:"defineCatalog rejects entitlements that omit assignableSubjectTypes",fn:async()=>{try{ma({permissions:[{id:"no_audience",name:"No Audience",description:"missing audience"}]}),e(!1,"defineCatalog should reject a missing audience")}catch(Ne){e(Ne instanceof Error&&Ne.message.includes("assignableSubjectTypes"),"the authoring error names assignableSubjectTypes")}}},{name:"standalone has* default 'any' resolves no principals without a session -> unauthenticated/401",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt,De=ot(mt({id:"authz-any-test"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000);let Ve=await gs(new Request("https://example.test/"),De,"read");e(!Ve.allow,"default 'any' with no session is not allowed"),e(Ve.reason==="unauthenticated","no principal resolved -> unauthenticated"),e(Ve.subject===void 0,"no subject is reported when nothing resolved"),e(Ve.subjectType===void 0,"no subjectType is reported when nothing resolved"),e(Ve.response().status===401,"unauthenticated -> 401"),De.close?.()}},{name:"subjectType isolates a workload from a user that shares an id (workload:abc vs user:abc)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"abc",subjectType:"workload",targetType:"role",target:"ops"}),await kt(Le,ze,Ne,{subject:"abc",subjectType:"user",targetType:"role",target:"viewer"}),e((await ze.hasRole("abc","ops",void 0,"workload")).allow,"workload:abc has ops"),e((await ze.hasPermission("abc","manage",void 0,"workload")).allow,"workload:abc has manage via ops"),e(!(await ze.hasRole("abc","ops",void 0,"user")).allow,"user:abc does NOT have ops"),e(!(await ze.hasPermission("abc","manage",void 0,"user")).allow,"user:abc does NOT inherit manage"),e((await ze.hasRole("abc","viewer",void 0,"user")).allow,"user:abc has viewer"),e(!(await ze.hasRole("abc","viewer",void 0,"workload")).allow,"workload:abc does NOT have viewer"),await ze.clear("abc","workload"),e(!(await ze.hasRole("abc","ops",void 0,"workload")).allow,"workload:abc edges cleared"),e((await ze.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 Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"svc-1",subjectType:"workload",targetType:"role",target:"ops"});let De=ot(mt({id:"authz-workload-test"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),e((await De.authz.hasRole("svc-1","ops",void 0,"workload")).allow,"workload grant resolves under the workload subjectType"),e((await De.authz.hasPermission("svc-1","manage",void 0,"workload")).allow,"workload inherits manage via ops"),e(!(await De.authz.hasRole("svc-1","ops",void 0,"user")).allow,"same id under user subjectType is denied"),De.close?.()}},{name:"accessRootId and store writes fail closed when subjectType is omitted",fn:async()=>{let Ne=!1;try{ys({subject:"x",targetType:"role",target:"viewer"})}catch(ze){Ne=ze instanceof Error&&ze.message.includes("subjectType is required")}e(Ne,"accessRootId rejects a missing subjectType instead of defaulting to user"),Ne=!1;try{Vd(void 0)}catch(ze){Ne=ze instanceof Error&&ze.message.includes("subjectType is required")}e(Ne,"assertRequiredSubjectType rejects undefined");let Le=new it;Ne=!1;try{await Le.grant({subject:"x",targetType:"role",target:"viewer"})}catch(ze){Ne=ze instanceof Error&&ze.message.includes("subjectType is required")}e(Ne,"assignment store grant rejects a missing subjectType")}},{name:"standalone hasScope* return unauthenticated -> 401 when no workload token is present",fn:async()=>{let Ne=ot(mt({id:"authz-scope-noauth-test"}),{validators:rt()});await Ne.ready(1000);let Le=new Request("https://example.test/"),ze=await $o(Le,Ne,"oaa:sync","workload");e(!ze.allow&&ze.reason==="unauthenticated","hasScope with no token -> unauthenticated"),e(ze.response().status===401,"hasScope unauthenticated -> 401");let De=await Jr(Le,Ne,["a","b"],"workload");e(!De.allow&&De.reason==="unauthenticated","hasAnyScope with no token -> unauthenticated");let Ve=await qo(Le,Ne,["a","b"],"workload");e(!Ve.allow&&Ve.reason==="unauthenticated","hasAllScope with no token -> unauthenticated"),Ne.close?.()}},{name:"per-issuer claimsAsEntitlements is read into ion.config and is absent by default",fn:async()=>{let Ne=ot(mt({id:"claims-block-on",workload:{jwksUri:"https://idp.example.test/certs",audience:"api",claimsAsEntitlements:{claims:["scope"]}}}),{validators:rt()});await Ne.ready(1000);let Le=Ne.config?.workload;e(Le?.claimsAsEntitlements?.claims?.[0]==="scope","the claims block is applied to ion.config"),Ne.close?.();let ze=ot(mt({id:"claims-block-off"}),{validators:rt()});await ze.ready(1000),e(!ze.config?.sso?.claimsAsEntitlements,"user-plane claims block defaults to unset"),ze.close?.()}},{name:"ion.authz.reconcile() re-derives the closure after a role->permission catalog edit (§1.2a)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"drift",subjectType:"user",targetType:"role",target:"viewer"});let De=ot(mt({id:"reconcile-roleedit"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),e((await De.authz.hasPermission("drift","read",void 0,"user")).allow,"viewer confers read"),e(!(await De.authz.hasPermission("drift","write",void 0,"user")).allow,"viewer does not confer write yet"),await Ne.catalog.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read","write"],assignableSubjectTypes:["user"]}).ready();let Ve=await De.authz.reconcile();e(Ve.roots===1,"one live root re-expanded (only subject in the system)"),e((await De.authz.hasPermission("drift","write",void 0,"user")).allow,"reconcile materializes the new write edge"),e((await De.authz.hasPermission("drift","read",void 0,"user")).allow,"read still present after reconcile"),De.close?.()}},{name:"ion.authz.reconcile() extends an applyToSubResources closure to a newly added child resource (§1.2a)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"tree",subjectType:"user",targetType:"permission",target:"manage",resources:["project-a"]});let De=ot(mt({id:"reconcile-tree"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),e((await De.authz.hasPermission("tree","manage","module-1","user")).allow,"manage cascades to the existing child module-1"),e(!(await De.authz.hasPermission("tree","manage","module-2","user")).allow,"module-2 does not exist yet"),Ne.resources.set({id:"module-2",type:"module",name:"Module 2",description:"M2",parent:"project-a"}),Ne.resources.set({id:"project-a",type:"project",name:"Project A",description:"A",resources:["module-1","module-2"]}),await De.authz.reconcile(),e((await De.authz.hasPermission("tree","manage","module-2","user")).allow,"reconcile cascades manage to the newly added child"),De.close?.()}},{name:"ion.authz.reconcile() rebuilds set-attributed edges so an overlapping permission survives one revoke",fn:async()=>{let Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"olap",subjectType:"user",targetType:"role",target:"editor"}),await kt(Le,ze,Ne,{subject:"olap",subjectType:"user",targetType:"role",target:"viewer"});let De=ot(mt({id:"reconcile-overlap"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),await Le.revoke({subject:"olap",subjectType:"user",targetType:"role",target:"editor"});let Ve=await De.authz.reconcile();e(Ve.roots===1,"only the surviving viewer root remains live"),e((await De.authz.hasPermission("olap","read",void 0,"user")).allow,"read survives via the viewer root"),e(!(await De.authz.hasPermission("olap","write",void 0,"user")).allow,"write is removed with the editor root"),De.close?.()}},{name:"reconcile atomically swaps a subject closure without a transient deny window",fn:async()=>{let Ne=await wt(),Le=new it,ze=new Kr;await kt(Le,ze,Ne,{subject:"atomic",subjectType:"user",targetType:"role",target:"editor"});let De=ot(mt({id:"reconcile-atomic"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await De.ready(1000),await Le.revoke({subject:"atomic",subjectType:"user",targetType:"role",target:"editor"});let Ve={subject:"atomic",subjectType:"user",targetType:"role",target:"viewer",status:"active"};Ve.id=ys(Ve),await Le.grant(Ve);let Je=De.authz.reconcile();await ze.replacementStarted;try{e((await ze.hasPermission("atomic","write",void 0,"user")).allow,"the previous complete closure remains visible while its replacement is pending")}finally{ze.releaseReplacement()}await Je,e((await ze.hasPermission("atomic","read",void 0,"user")).allow,"the replacement viewer closure is committed"),e(!(await ze.hasPermission("atomic","write",void 0,"user")).allow,"the stale editor-only permission disappears at the atomic swap"),De.close?.()}},{name:"partial-failure retry converges: a failed index materialize is repaired by re-grant + re-materialize (replaces §1.2b)",fn:async()=>{let Ne=await wt(),Le=new it,ze=new qn,De={subject:"retry",subjectType:"user",targetType:"role",target:"viewer",status:"active"};De.id=ys(De),await Le.grant(De);let Ve=!1;try{let Ge=await Ms(De,Ne);await ze.materialize(De.id,Ge)}catch{Ve=!0}e(Ve,"the first index write fails (simulated transient error)"),e(!(await ze.hasPermission("retry","read",void 0,"user")).allow,"no derived access after the failed attempt"),await Le.grant(De);let Je=await Ms(De,Ne);await ze.materialize(De.id,Je),e((await ze.hasPermission("retry","read",void 0,"user")).allow,"retry re-materializes the closure"),e((await Le.listBySubject("retry","user")).items.length===1,"exactly one root persisted (grant is idempotent; no duplicate)"),await ze.removeByRoot(De.id),e(!(await ze.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 Ne=await wt(),Le=new it,ze=new lt,De=await kt(Le,ze,Ne,{subject:"idem",subjectType:"user",targetType:"role",target:"viewer"});e((await ze.hasPermission("idem","read",void 0,"user")).allow,"granted read is present"),await Le.revoke({subject:"idem",subjectType:"user",targetType:"role",target:"viewer"}),await ze.removeByRoot(De),e(!(await ze.hasPermission("idem","read",void 0,"user")).allow,"read denied after revoke"),await Le.revoke({subject:"idem",subjectType:"user",targetType:"role",target:"viewer"}),await ze.removeByRoot(De),e((await Le.listBySubject("idem","user")).items.length===0,"no roots remain after idempotent revoke"),e(!(await ze.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 Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"leaver",subjectType:"user",targetType:"group",target:"staff"}),await kt(Le,ze,Ne,{subject:"leaver",subjectType:"user",targetType:"role",target:"ops"}),e((await ze.hasPermission("leaver","read",void 0,"user")).allow,"leaver inherits read via staff"),e((await ze.hasPermission("leaver","manage",void 0,"user")).allow,"leaver holds manage via ops");let De=await Le.listBySubject("leaver","user");e(De.items.length===2,"both of the leaver roots are enumerated by listBySubject");for(let Ve of De.items)await Le.revoke(Ve),await ze.removeByRoot(Ve.id??ys(Ve));e(!(await ze.hasGroup("leaver","staff",void 0,"user")).allow,"group membership revoked on delete"),e(!(await ze.hasPermission("leaver","read",void 0,"user")).allow,"inherited read denied on delete"),e(!(await ze.hasPermission("leaver","manage",void 0,"user")).allow,"role permission denied on delete"),e((await Le.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 Ne=await wt(),Le=new it,ze=new lt,De={subject:"live",subjectType:"user",targetType:"role",target:"editor",status:"active"};De.id=ys(De),await Le.grant(De),e(!(await ze.hasPermission("live","read",void 0,"user")).allow,"live subject has no index rows pre-reconcile"),await ze.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"}]),e((await ze.hasPermission("orphan","read",void 0,"user")).allow,"orphan rows are present pre-reconcile");let Ve=ot(mt({id:"reconcile-full"}),{validators:rt(),stores:{accessStore:Le,accessIndex:ze,catalogStore:Ne.catalog,resources:Ne.resources}});await Ve.ready(1000);let Je=await Ve.authz.reconcile();e(Je.subjects>=2,"the full reconcile visited both the live and orphan subjects"),e((await Ve.authz.hasPermission("live","read",void 0,"user")).allow,"full reconcile materializes the live root"),e((await Ve.authz.hasPermission("live","write",void 0,"user")).allow,"editor closure rebuilt for the live subject"),e(!(await Ve.authz.hasPermission("orphan","read",void 0,"user")).allow,"orphan index rows are swept"),e(Je.cleared>=1,"the orphan subject is reported as cleared"),Ve.close?.()}},{name:"§8.4 id parity: two roots with the same subject/target but different resources do NOT collapse",fn:async()=>{let Ne={subject:"pat",subjectType:"user",targetType:"role",target:"editor",resources:["region:north"]},Le={subject:"pat",subjectType:"user",targetType:"role",target:"editor",resources:["region:south"]};e(ys(Ne)!==ys(Le),"different resource scopes yield different root ids"),e(ys(Ne)===ys({...Ne}),"same inputs yield a stable root id");let ze=new it;await ze.grant({...Ne,id:ys(Ne),status:"active"}),await ze.grant({...Le,id:ys(Le),status:"active"});let De=await ze.listByTarget("role","editor");e(De.items.length===2,"both resource-scoped roots coexist (no collapse)"),e(zr(" editor ")==="editor","accessRefId trims string ids"),e(zr({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 Ne=0,Le=0;class ze extends it{async listByTarget(...Je){return Ne+=1,super.listByTarget(...Je)}async listByTargets(...Je){return Le+=1,super.listByTargets(...Je)}}let De=new ze;await De.grant({subject:"m1",subjectType:"user",targetType:"group",target:"g1",status:"active"}),await De.grant({subject:"m2",subjectType:"user",targetType:"group",target:"g1",status:"active"}),await De.grant({subject:"m3",subjectType:"user",targetType:"role",target:"r1",status:"active"});let Ve=await De.listByTargets([{targetType:"group",target:"g1"},{targetType:"role",target:"r1"},{targetType:"permission",target:"p1"}]);e(Le===1,"one batched call covers the whole page"),e(Ne===0,"the batched path does not fan out to per-item listByTarget"),e((Ve.get("group:g1")??[]).length===2,"g1 members batched"),e((Ve.get("role:r1")??[]).length===1,"r1 members batched"),e((Ve.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 Ne=await wt(),Le=new it,ze=new lt;await kt(Le,ze,Ne,{subject:"multi",subjectType:"user",targetType:"role",target:"editor"}),await kt(Le,ze,Ne,{subject:"multi",subjectType:"user",targetType:"role",target:"viewer"}),await kt(Le,ze,Ne,{subject:"multi",subjectType:"user",targetType:"role",target:"ops"});let De=["editor","viewer","ops"].map((Je)=>({targetType:"role",target:Je}));e((await ze.hasAll("multi",De,"user")).allow,"hasAll is satisfied when every target is held");let Ve=[...De,{targetType:"role",target:"admin"}];e(!(await ze.hasAll("multi",Ve,"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 Ne=await wt(),Le={store:new it,index:new lt},ze={store:new it,index:new lt};await kt(Le.store,Le.index,Ne,{subject:"shared-123",subjectType:"user",targetType:"role",target:"ops"}),e((await Le.index.hasRole("shared-123","ops",void 0,"user")).allow,"tenant A granted ops to shared-123"),e(!(await ze.index.hasRole("shared-123","ops",void 0,"user")).allow,"tenant B (separate keyspace) does NOT see tenant A grant for the same id"),e((await ze.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 Ne=await wt(),Le={subject:"memo",subjectType:"user",targetType:"group",target:"staff"};Le.id=ys(Le);let ze=await Ms(Le,Ne),De=new Dr(Ne.catalog,Ne.resources),Ve={...Ne,cache:De},Je=await Ms(Le,Ve),Ge=await Ms(Le,Ve);e(On(ze,Je)&&On(Je,Ge),"cached expansion produces the same edges as the uncached read"),await Ne.catalog.groups.set({id:"staff",name:"Staff",description:"Staff",roles:["editor","ops"],assignableSubjectTypes:["user"]}).ready();let We=await Ms(Le,Ve);e(On(We,Je),"a not-yet-invalidated memo still returns the old closure"),De.invalidate();let Ke=await Ms(Le,Ve);e(Ke.some((Fe)=>Fe.targetType==="permission"&&Fe.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 Ne=Ws(),Le=new it,ze=new lt;await kt(Le,ze,{code:Ne},{subject:"carol",subjectType:"user",targetType:"group",target:"code_staff"}),e((await ze.hasGroup("carol","code_staff",void 0,"user")).allow,"carol is in the code-declared group"),e((await ze.hasRole("carol","code_editor",void 0,"user")).allow,"the code group confers the code role"),e((await ze.hasPermission("carol","code_read",void 0,"user")).allow,"the code role confers code_read"),e((await ze.hasPermission("carol","code_write",void 0,"user")).allow,"the code role confers code_write"),e(Ne.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 Ne=(Le)=>{try{return Le(),!1}catch{return!0}};e(Ne(()=>ma({roles:[{id:"dup",name:"Dup",description:"dup",assignableSubjectTypes:["user"]}],permissions:[{id:"dup",name:"Dup",description:"dup",assignableSubjectTypes:["user"]}]})),"a duplicate id across kinds throws (one SCIM Groups id space)"),e(Ne(()=>ma({groups:[{id:"g",name:"G",description:"g",assignableSubjectTypes:["user"],roles:["missing_role"]}]})),"a dangling group->role reference throws"),e(Ne(()=>ma({groups:[{id:"a",name:"A",description:"a",assignableSubjectTypes:["user"],groups:["b"]},{id:"b",name:"B",description:"b",assignableSubjectTypes:["user"],groups:["a"]}]})),"a nested-group cycle throws")}},{name:"§7.2 stableConfigHash is order-independent and agrees with deepEqualPlain equality",fn:async()=>{let Ne={id:"t",authz:{trustedProvisioningClients:[]},workload:{jwksUri:"https://x/certs"}},Le={workload:{jwksUri:"https://x/certs"},authz:{trustedProvisioningClients:[]},id:"t"},ze={...Ne,extra:void 0},De={...Ne,id:"t2"};e(On(Ne,Le),"precondition: the two objects are deep-equal"),e(xa(Ne)===xa(Le),"key order does not change the hash"),e(xa(Ne)===xa(ze),"undefined-valued keys do not change the hash"),e(xa(Ne)!==xa(De),"a changed value changes the hash")}},{name:'§10.3 escapeScimFilterValue backslash-escapes `"` and `\\` for RFC 7644 filters',fn:async()=>{t(Za('a"b'),"a\\\"b","a double quote is backslash-escaped so it stays inside the filter literal"),t(Za("a\\b"),"a\\\\b","a backslash is doubled so the escape is unambiguous"),t(Za("x\\\"y"),"x\\\\\\\"y","ordering escapes backslash first, then quote"),t(Za("plain-id"),"plain-id","an ordinary id is returned unchanged");let Ne=`externalId eq "${Za('ev"il')}"`;t(Ne,'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 Ne=new St;await Ne.permissions.set({id:"read",name:"Read",description:"Read",assignableSubjectTypes:["user"]}).ready(),await Ne.permissions.set({id:"write",name:"Write",description:"Write",assignableSubjectTypes:["user"]}).ready(),await Ne.roles.set({id:"editor",name:"Editor",description:"Editor",permissions:["read","write"],assignableSubjectTypes:["user"]}).ready(),await Ne.groups.set({id:"staff",name:"Staff",description:"Staff",roles:["editor"],assignableSubjectTypes:["user"]}).ready();let Le=0,ze=(Fe)=>({...Fe,get:async(He)=>(Le+=1,Fe.get(He))}),De={groups:ze(Ne.groups),roles:ze(Ne.roles),permissions:ze(Ne.permissions)},Ve=Array.from({length:100},(Fe,He)=>`m${He}`),Je=(Fe)=>({subject:Fe,subjectType:"user",targetType:"group",target:"staff",status:"active",id:`r-${Fe}`});Le=0;for(let Fe of Ve)await Ms(Je(Fe),{catalog:De});let Ge=Le,We=new Dr(De);Le=0;for(let Fe of Ve)await Ms(Je(Fe),{catalog:De,cache:We});let Ke=Le;e(Ge>=Ve.length,`uncached expansion re-reads the graph per member (saw ${Ge})`),e(Ke<Ge,"a shared adjacency cache must collapse repeated node reads"),e(Ke<=8,`cached expansion reads the shared graph O(graph) once, not O(members×graph) (saw ${Ke})`)}}]}}function Hr(Be){let Ne=typeof Be==="function"?Be():Be;if(!Ne)throw Error("AuthzValidationConfig.ecv is required to run OAA projection tests");return Ne}function No(Be,Ne){return mt({id:"ecv-oaa-projection",authz:{oaa:{url:Be.baseUrl,apiKey:Be.oaa.apiKey,providerId:Be.oaa.providerId,datasourceId:Be.oaa.datasourceId,applicationName:"ECV OAA Projection",users:{population:"all"},customers:{population:"all"},workloads:{population:"all"}},...Ne?{trustedProvisioningClients:Ne}:{}}})}async function $s(Be,Ne,Le,ze){let De=ot(No(Be,ze),{validators:rt()});try{return await De.ready(Ne),await Le(De)}finally{De.close?.()}}async function Uo(Be,Ne,Le,ze,De){let Ve=ot(No(Be),{validators:rt(),stores:{catalogStore:Le,...De?{resources:De}:{}}});try{return await Ve.ready(Ne),await ze(Ve)}finally{Ve.close?.()}}function Qa(Be,Ne){return mt({id:"ecv-oaa-fullsync",workload:{jwksUri:`${Be.baseUrl}/workload/certs`,audience:`${Be.baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},authz:{oaa:{url:Be.baseUrl,apiKey:Be.oaa.apiKey,providerId:Be.oaa.providerId,datasourceId:Be.oaa.datasourceId,applicationName:"ECV OAA Full Sync",fullSync:{enabled:Ne}}}})}function en(Be,Ne,Le={}){return mt({id:Ne,workload:{jwksUri:`${Be.baseUrl}/workload/certs`,audience:`${Be.baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{},authz:{oaa:{url:Be.baseUrl,apiKey:Be.oaa.apiKey,providerId:Be.oaa.providerId,datasourceId:Be.oaa.datasourceId,applicationName:"ECV SCIM OAA Guard",...Le}}})}var Ea="https://ecv.test/api/ionite/authz/oaa/sync";function Vt(Be){let Ne=Be.applications;if(!Array.isArray(Ne)||Ne.length===0)return;let Le=Ne[0];return Le&&typeof Le==="object"?Le:void 0}function ht(Be){if(!Array.isArray(Be))return[];return Be.filter((Ne)=>Boolean(Ne)&&typeof Ne==="object")}function p(Be){let Ne=Be.timeout??5000,Le=()=>Hr(Be.ecv);return[{name:"SCIM Group member type Workload grants a workload root and round-trips as type Workload",fn:async()=>{let ze=await gt(Le().baseUrl,`${Gt.id} ${Qt.id}`),De=new St;await De.permissions.set({id:"read",name:"Read",description:"Read",source:"app",assignableSubjectTypes:["user"]}).ready(),await De.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app",assignableSubjectTypes:["user"]}).ready(),await De.groups.set({id:"vault_path_reader",name:"Path Reader",description:"Path Reader",requestable:!0,assignableSubjectTypes:["workload"],roles:["viewer"],source:"app"}).ready();let Ve=new it,Je=new lt,Ge=ot(mt({id:"ecv-scim-workload-member",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}}),{validators:rt(),stores:{catalogStore:De,accessStore:Ve,accessIndex:Je}});try{await Ge.ready(1000);let We=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/vault_path_reader",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-vault",type:"Workload"}]}]})}));e(We.status===200,`workload member PATCH should succeed, got ${We.status}: ${await We.text()}`),e((await Je.hasPermission("svc-vault","read",void 0,"workload")).allow,"Workload member satisfies a workload permission check"),e(!(await Je.hasPermission("svc-vault","read",void 0,"user")).allow,"same-id user check does not see the workload grant");let Ke=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/vault_path_reader",{headers:{Authorization:`Bearer ${ze}`}}));e(Ke.status===200,`GET group should succeed, got ${Ke.status}`);let He=(await Ke.json()).members?.find((Xe)=>Xe.value==="svc-vault");e(He?.type==="Workload",`projected member type should be Workload, got ${He?.type}`)}finally{Ge.close?.()}}},{name:"SCIM Group member type Customer grants a customer root and round-trips as type Customer",fn:async()=>{let ze=await gt(Le().baseUrl,`${Gt.id} ${Qt.id}`),De=new St;await De.permissions.set({id:"read",name:"Read",description:"Read",source:"app",assignableSubjectTypes:["customer"]}).ready(),await De.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app",assignableSubjectTypes:["customer"]}).ready(),await De.groups.set({id:"customer_club",name:"Customer Club",description:"Customer Club",requestable:!0,assignableSubjectTypes:["customer"],roles:["viewer"],source:"app"}).ready();let Ve=new it,Je=new lt,Ge=ot(mt({id:"ecv-scim-customer-member",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}}),{validators:rt(),stores:{catalogStore:De,accessStore:Ve,accessIndex:Je}});try{await Ge.ready(1000);let We=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/customer_club",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"cust-club",type:"Customer"}]}]})}));e(We.status===200,`customer member PATCH should succeed, got ${We.status}: ${await We.text()}`),e((await Je.hasPermission("cust-club","read",void 0,"customer")).allow,"Customer member satisfies a customer permission check");let Ke=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/customer_club",{headers:{Authorization:`Bearer ${ze}`}}));e(Ke.status===200,`GET group should succeed, got ${Ke.status}`);let He=(await Ke.json()).members?.find((Xe)=>Xe.value==="cust-club");e(He?.type==="Customer",`projected member type should be Customer, got ${He?.type}`)}finally{Ge.close?.()}}},{name:"ion.authz.grant rejects iam assignment policy with requestUrl; SCIM ingress stays exempt",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new St;await De.permissions.set({id:"read",name:"Read",description:"Read",source:"app",assignableSubjectTypes:["customer"]}).ready(),await De.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app",assignableSubjectTypes:["customer"]}).ready(),await De.groups.set({id:"customer_access",name:"Customer Access",description:"Customer Access",requestable:!0,requestUrl:"https://iam.example.test/request/customer_access",assignableSubjectTypes:["customer"],roles:["viewer"],source:"app"}).ready();let Ve=new it,Je=new lt,Ge=ot(mt({id:"ecv-assignment-policy",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{customers:{assignment:"iam"}}}),{validators:rt(),stores:{catalogStore:De,accessStore:Ve,accessIndex:Je}});try{await Ge.ready(1000),e(Ge.iam?.customers.assignment==="iam","normalized accessor exposes the Ops policy");try{await Ge.authz.grant({subject:"cust-denied",subjectType:"customer",targetType:"group",target:"customer_access"}),e(!1,"ion.authz.grant should reject when assignment is iam")}catch(Ke){e(Ke instanceof Un,"policy deny is a typed AssignmentPolicyError"),e(Ke.policy==="iam","error carries the active policy"),e(Ke.requestUrl==="https://iam.example.test/request/customer_access","error carries the catalog requestUrl receipt")}let We=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/customer_access",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"cust-scim",type:"Customer"}]}]})}));e(We.status===200,`SCIM ingress stays policy-exempt, got ${We.status}: ${await We.text()}`),e((await Je.hasGroup("cust-scim","customer_access",void 0,"customer")).allow,"IAM-assigned customer member is granted")}finally{Ge.close?.()}}},{name:"requestAccess is allowed only when assignment is iam-request",fn:async()=>{let ze=new St;await ze.groups.set({id:"beta_access",name:"Beta access",description:"Beta access",requestable:!0,assignableSubjectTypes:["customer"],source:"app"}).ready();let De=new it,Ve=new lt,Je=ot(mt({id:"ecv-request-access-app",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{customers:{assignment:"app"}}}),{validators:rt(),stores:{catalogStore:ze,accessStore:De,accessIndex:Ve}});try{await Je.ready(1000);for(let We of["grant","revoke"])try{await Je.authz.requestAccess({subject:"cust-req",subjectType:"customer",targetType:"group",target:"beta_access",operation:We}),e(!1,`${We} requestAccess should reject when assignment is app`)}catch(Ke){e(Ke instanceof Un,"policy deny is a typed AssignmentPolicyError"),e(Ke.policy==="app","error carries the active app policy")}}finally{Je.close?.()}let Ge=ot(mt({id:"ecv-request-access-iam-request",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{customers:{assignment:"iam-request"}}}),{validators:rt(),stores:{catalogStore:ze,accessStore:De,accessIndex:Ve}});try{await Ge.ready(1000),e(Ge.iam?.customers.assignment==="iam-request","accessor exposes iam-request");try{await Ge.authz.grant({subject:"cust-req",subjectType:"customer",targetType:"group",target:"beta_access"}),e(!1,"grant should reject when assignment is iam-request")}catch(We){e(We instanceof Un,"grant deny is a typed AssignmentPolicyError"),e(We.policy==="iam-request","error carries iam-request"),e(We instanceof Error&&We.message.includes("requestAccess"),"iam-request grant error points at requestAccess"),e(We.ionId==="ecv-request-access-iam-request","the error is stamped with the tenant runtime id"),e(We.log===Ge.log,"the error carries the runtime logger");let Ke=We.response(),Fe=await Ke.json();t(Ke.status,403,"assignment policy is a 403"),t(Fe.error,"assignment_forbidden","the client sees the stable code, not Error.message"),t(Fe.policy,"iam-request","the client sees the policy"),e(!("message"in Fe),"Error.message is not a client field");let He=JSON.stringify(We);e(!He.includes("log"),"serializing the error does not include the logger"),e(!He.includes("requestAccess"),"serializing the error does not include the server message")}}finally{Ge.close?.()}}},{name:"iam-request requestAccess reuses pending tuples and needs only the IAM destination channel, not delta",fn:async()=>{let ze=new St;await ze.groups.set({id:"beta_access",name:"Beta access",description:"Beta access",requestable:!0,assignableSubjectTypes:["customer"],source:"app"}).ready();let De=()=>({catalogStore:ze,accessStore:new it,accessIndex:new lt}),Ve={idpTokenUrl:`${Le().baseUrl}/workload/token`,clientId:"local-workload-client",clientSecret:"local-workload-secret",autoRefresh:!1},Je=ot(mt({id:"ecv-iam-request-pull-only",workload:Ve,iam:{url:`${Le().baseUrl}/iam`,customers:{assignment:"iam-request"}}}),{validators:rt(),stores:De()});try{await Je.ready(1000),e(Je.iam?.delta?.isReady()!==!0,"delta egress stays not-ready with no kind on patch");let We=(Ze)=>Je.authz.requestAccess({subject:"cust-req",subjectType:"customer",targetType:"group",target:"beta_access",operation:Ze}),Ke=await We("grant"),Fe=await We("grant"),He=await We("revoke");e(Ke.status==="pending","the created request is pending"),e(Ke.id===Fe.id,"a pending request for the same tuple is reused"),e(Ke.operation==="grant"&&He.operation==="revoke","operations round-trip"),e(He.id!==Ke.id,"grant and revoke are distinct request tuples"),e(typeof Ke.trackingUrl==="string"&&Ke.trackingUrl.length>0,"trackingUrl is set");let Xe=await fetch(`${Le().baseUrl}/iam/AccessRequests/${encodeURIComponent(Ke.id)}`,{headers:{Authorization:"Bearer ecv-test"}});e(Xe.status===200,"the submission reaches the IAM destination AccessRequests endpoint");let Ye=await Je.authz.getAccessRequest({id:Ke.id,subjectType:"customer"});e(Ye.id===Ke.id&&Ye.status==="pending","IAM request read-back preserves state")}finally{Je.close?.()}let Ge=ot(mt({id:"ecv-iam-request-no-url",workload:Ve,iam:{customers:{assignment:"iam-request"}}}),{validators:rt(),stores:De()});try{await Ge.ready(1000);try{await Ge.authz.requestAccess({subject:"cust-req",subjectType:"customer",targetType:"group",target:"beta_access",operation:"revoke"}),e(!1,"requestAccess should reject without an IAM destination URL")}catch(We){e(We instanceof Error&&We.message.includes("IAM destination"),"the missing-url rejection names the destination channel precondition")}}finally{Ge.close?.()}}},{name:"IdP user assignment rejects direct grant and routes request submission and read-back through SSO",fn:async()=>{let ze=new St;await ze.groups.set({id:"workforce_beta",name:"Workforce beta",description:"Workforce beta",requestable:!0,assignableSubjectTypes:["user"],source:"app"}).ready();let De=ot(mt({id:"ecv-idp-user",workload:{idpTokenUrl:`${Le().baseUrl}/workload/token`,clientId:"local-workload-client",clientSecret:"local-workload-secret",autoRefresh:!1},sso:{clientId:"ecv-client",clientSecret:"ecv-secret",claimsAsEntitlements:{claims:["groups"]},accessRequests:{url:`${Le().baseUrl}/sso`}},iam:{users:{assignment:"idp"}}}),{validators:rt(),stores:{catalogStore:ze,accessStore:new it,accessIndex:new lt}});try{await De.ready(1000);try{await De.authz.grant({subject:"user-idp",subjectType:"user",targetType:"group",target:"workforce_beta"}),e(!1,"grant should reject when assignment is 'idp'")}catch(Ke){e(Ke instanceof Un,"IdP policy denial is typed"),e(Ke.policy==="idp","the policy error carries 'idp'")}let Ve=await De.authz.requestAccess({subject:"user-idp",subjectType:"user",targetType:"group",target:"workforce_beta",operation:"grant"});e(Ve.status==="pending","SSO should return a pending access request");let Je=await fetch(`${Le().baseUrl}/sso/AccessRequests/${encodeURIComponent(Ve.id)}`,{headers:{Authorization:"Bearer ecv-test"}}),Ge=await fetch(`${Le().baseUrl}/iam/AccessRequests/${encodeURIComponent(Ve.id)}`,{headers:{Authorization:"Bearer ecv-test"}});e(Je.status===200&&Ge.status===404,"the request should exist only on the SSO channel");let We=await De.authz.getAccessRequest({id:Ve.id,subjectType:"user"});e(We.id===Ve.id&&We.status==="pending","SSO request read-back should preserve state")}finally{De.close?.()}}},{name:"IdP customer assignment selects one CIAM provider and requires provider when multiple are configured",fn:async()=>{let ze=new St;await ze.groups.set({id:"customer_beta",name:"Customer beta",description:"Customer beta",requestable:!0,assignableSubjectTypes:["customer"],source:"app"}).ready();let De=(Ge)=>({type:"oidc",clientId:"ecv-client",clientSecret:"ecv-secret",claimsAsEntitlements:{claims:["groups"]},accessRequests:{url:Ge}}),Ve=ot(mt({id:"ecv-idp-customer",workload:{idpTokenUrl:`${Le().baseUrl}/workload/token`,clientId:"local-workload-client",clientSecret:"local-workload-secret",autoRefresh:!1},ciam:{providers:{alpha:De(`${Le().baseUrl}/sso`),beta:De(`${Le().baseUrl}/iam`)}},iam:{customers:{assignment:"idp"}}}),{validators:rt(),stores:{catalogStore:ze,accessStore:new it,accessIndex:new lt}});try{await Ve.ready(1000);try{await Ve.authz.requestAccess({subject:"customer-idp",subjectType:"customer",targetType:"group",target:"customer_beta",operation:"grant"}),e(!1,"multiple CIAM destinations should require provider selection")}catch(He){e(He instanceof Error&&He.message.includes("provider"),"the error should require provider")}let Ge=await Ve.authz.requestAccess({subject:"customer-idp",subjectType:"customer",targetType:"group",target:"customer_beta",operation:"grant",provider:"beta"}),We=await fetch(`${Le().baseUrl}/iam/AccessRequests/${encodeURIComponent(Ge.id)}`,{headers:{Authorization:"Bearer ecv-test"}}),Ke=await fetch(`${Le().baseUrl}/sso/AccessRequests/${encodeURIComponent(Ge.id)}`,{headers:{Authorization:"Bearer ecv-test"}});e(We.status===200&&Ke.status===404,"the selected provider should route to IAM");let Fe=await Ve.authz.getAccessRequest({id:Ge.id,subjectType:"customer",provider:"beta"});e(Fe.id===Ge.id,"the selected CIAM provider should serve request read-back")}finally{Ve.close?.()}let Je=ot(mt({id:"ecv-idp-customer-single",workload:{idpTokenUrl:`${Le().baseUrl}/workload/token`,clientId:"local-workload-client",clientSecret:"local-workload-secret",autoRefresh:!1},ciam:{providers:{alpha:De(`${Le().baseUrl}/sso`)}},iam:{customers:{assignment:"idp"}}}),{validators:rt(),stores:{catalogStore:ze,accessStore:new it,accessIndex:new lt}});try{await Je.ready(1000);let Ge=await Je.authz.requestAccess({subject:"customer-single",subjectType:"customer",targetType:"group",target:"customer_beta",operation:"grant"}),We=await fetch(`${Le().baseUrl}/sso/AccessRequests/${encodeURIComponent(Ge.id)}`,{headers:{Authorization:"Bearer ecv-test"}});e(We.status===200,"one CIAM destination is implicit and routes to its SSO-backed mock")}finally{Je.close?.()}}},{name:"SCIM typeless member resolves when assignableSubjectTypes has exactly one kind and is rejected when ambiguous",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new St;await De.permissions.set({id:"read",name:"Read",description:"Read",source:"app",assignableSubjectTypes:["user"]}).ready(),await De.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app",assignableSubjectTypes:["user"]}).ready(),await De.groups.set({id:"workload_only",name:"Workload Only",description:"Workload Only",requestable:!0,assignableSubjectTypes:["workload"],roles:["viewer"],source:"app"}).ready(),await De.groups.set({id:"open_entitlement",name:"Open Entitlement",description:"Open Entitlement",requestable:!0,assignableSubjectTypes:["user","workload"],roles:["viewer"],source:"app"}).ready();let Ve=new it,Je=new lt,Ge=ot(mt({id:"ecv-scim-typeless-member",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}}),{validators:rt(),stores:{catalogStore:De,accessStore:Ve,accessIndex:Je}});try{await Ge.ready(1000);let We=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/workload_only",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-typeless"}]}]})}));e(We.status===200,`typeless member on a workload-only entitlement should resolve, got ${We.status}: ${await We.text()}`),e((await Je.hasPermission("svc-typeless","read",void 0,"workload")).allow,"typeless member resolved to workload from assignableSubjectTypes");let Ke=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/open_entitlement",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"svc-ambiguous"}]}]})}));e(Ke.status===400,`typeless member on a multi-kind entitlement should 400, got ${Ke.status}`);let Fe=await Ke.text();e(Fe.includes("open_entitlement"),"ambiguity error names the entitlement"),e((await Ve.listBySubject("svc-ambiguous","user")).items.length===0,"ambiguous member wrote no user root"),e((await Ve.listBySubject("svc-ambiguous","workload")).items.length===0,"ambiguous member wrote no workload root")}finally{Ge.close?.()}}},{name:"same-id User and Workload members remain distinct roots",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new St;await De.permissions.set({id:"read",name:"Read",description:"Read",source:"app",assignableSubjectTypes:["user"]}).ready(),await De.roles.set({id:"viewer",name:"Viewer",description:"Viewer",permissions:["read"],source:"app",assignableSubjectTypes:["user"]}).ready(),await De.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 Ve=new it,Je=new lt,Ge=ot(mt({id:"ecv-scim-same-id-members",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}}),{validators:rt(),stores:{catalogStore:De,accessStore:Ve,accessIndex:Je}});try{await Ge.ready(1000);let We=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/shared_id_entitlement",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},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"}]}]})}));e(We.status===200,`same-id User+Workload PATCH should succeed, got ${We.status}: ${await We.text()}`),e((await Je.hasPermission("shared","read",void 0,"user")).allow,"user:shared holds the grant"),e((await Je.hasPermission("shared","read",void 0,"workload")).allow,"workload:shared holds a distinct grant");let Ke=await Ve.listBySubject("shared","user"),Fe=await Ve.listBySubject("shared","workload");e(Ke.items.length===1,"user principal has its own root"),e(Fe.items.length===1,"workload principal has its own root"),e(Ke.items[0]?.id!==Fe.items[0]?.id,"same-id User and Workload roots are distinct")}finally{Ge.close?.()}}},{name:"ion.authz.oaa is ready when pointed at the mock receiver and projects an incremental user push",fn:async()=>{await Le().oaa.clear(),await $s(Le(),Ne,async(Ge)=>{e(Ge.authz.oaa.isReady(),"OAA should be ready with mock receiver config"),await Ge.authz.oaa.ready(Ne),await Ge.authz.oaa.pushUser({id:"ecv-user-1",userName:"ecv-user-1@example.com",displayName:"ECV User One",email:"ecv-user-1@example.com"})});let ze=await Le().oaa.pushes();e(ze.length>=1,"expected at least one OAA push to be received");let De=ze[ze.length-1];e(De!==void 0,"expected a recorded OAA push"),e(De.via==="single","an incremental user push should use the single :push endpoint"),e(De.isIncremental===!0,"a mutation-scoped push should be marked incremental"),e(De.compressionType==="GZIP","OAA client compresses payloads with gzip by default");let Ve=Vt(De.payload);e(Ve!==void 0,"OAA payload must contain an application");let Je=ht(Ve?.local_users);e(Je.some((Ge)=>Ge.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 Le().oaa.clear(),await $s(Le(),Ne,async(We)=>{await We.authz.oaa.pushGroup({id:"ecv-group-1",name:"ECV Group One",description:"ECV projected group",roles:["ecv-role-1"],permissions:["ecv-perm-1"]})});let ze=(await Le().oaa.pushes()).at(-1);e(ze!==void 0,"expected a recorded OAA group push");let De=Vt(ze.payload),Ve=ht(De?.local_groups);e(Ve.some((We)=>We.unique_id==="ecv-group-1"),"the pushed group should appear in local_groups");let Ge=ht(ze.payload.identity_to_permissions).find((We)=>We.identity==="ecv-group-1");e(Ge!==void 0,"group role/permission assignments should project an identity entry"),e(Ge?.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 Le().oaa.clear(),await $s(Le(),Ne,async(He)=>{await He.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 ze=(await Le().oaa.pushes()).at(-1);e(ze!==void 0,"expected a recorded OAA group push");let Ve=ht(ze.payload.identity_to_permissions).find((He)=>He.identity==="ecv-region-librarian");e(Ve!==void 0,"resource-bound group should project an identity entry");let Ge=ht(Ve?.role_assignments).find((He)=>He.role==="ecv-librarian");e(Ge!==void 0,"the group should confer the librarian role"),e(Ge?.apply_to_application===!1,"a resource-bound assignment must not be application-wide"),e(Array.isArray(Ge?.resources)&&(Ge?.resources).includes("region:ecv-north"),"the role assignment carries the bound resource");let We=Vt(ze.payload),Fe=ht(We?.local_groups).find((He)=>He.unique_id==="ecv-region-librarian")?.custom_properties??{};e(Array.isArray(Fe.resources)&&Fe.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 Le().oaa.clear(),await $s(Le(),Ne,async(Je)=>{await Je.authz.oaa.pushPermission({id:"ecv-perm-1",name:"ECV Read",description:"ECV read permission",permissionTypes:["DataRead"],dataSteward:"permissions@example.test"})});let ze=(await Le().oaa.pushes()).at(-1);e(ze!==void 0,"expected a recorded OAA permission push");let Ve=ht(ze.payload.permissions).find((Je)=>Je.name==="ECV Read");e(Ve!==void 0,"the pushed permission should appear in the permissions stream"),e(Array.isArray(Ve?.permission_type)&&(Ve?.permission_type).includes("DataRead"),"permission_type should carry the canonical OAA privilege"),e(Ve?.custom_properties?.dataSteward==="permissions@example.test","incremental permission push projects flat custom attributes")}},{name:"ion.authz.oaa projects grant then revoke of a group membership",fn:async()=>{await Le().oaa.clear();let ze={subject:"ecv-user-2",subjectType:"user",targetType:"group",target:"ecv-group-1"};await $s(Le(),Ne,async(Ke)=>{await Ke.authz.oaa.grant(ze),await Ke.authz.oaa.revoke(ze)});let De=await Le().oaa.pushes();e(De.length>=2,"grant and revoke should each produce a push");let Je=ht(Vt(De.at(-2)?.payload??{})?.local_users).find((Ke)=>Ke.unique_id==="ecv-user-2");e(Je!==void 0,"grant should project the subject as a local_user"),e(Array.isArray(Je?.groups)&&(Je?.groups).includes("ecv-group-1"),"granted membership should list the target group"),e(Je?.operation==="add","grant projection uses the add operation");let We=ht(Vt(De.at(-1)?.payload??{})?.local_users).find((Ke)=>Ke.unique_id==="ecv-user-2");e(We?.operation==="delete","revoke projection uses the delete operation")}},{name:"ion.authz.oaa.sync uploads a full (non-incremental) snapshot",fn:async()=>{await Le().oaa.clear();let ze=await $s(Le(),Ne,async(Fe)=>Fe.authz.oaa.sync({users:[{id:"ecv-sync-user",userName:"ecv-sync-user@example.com",displayName:"ECV Sync User",name:"ECV Sync User",email:"ecv-sync-user@example.com",dataSteward:"users@example.test"}],customers:[{id:"ecv-sync-customer",userName:"ecv-sync-customer@example.com",name:"ECV Sync Customer",email:"ecv-sync-customer@example.com",dataSteward:"customers@example.test"}],workloads:[{id:"ecv-sync-workload",displayName:"ECV Sync Workload",dataSteward:"workloads@example.test"}],groups:[{id:"ecv-sync-group",name:"ECV Sync Group",description:"ECV sync group",assignableSubjectTypes:["user"],dataSteward:"groups@example.test"}],roles:[{id:"ecv-sync-role",name:"ECV Sync Role",description:"ECV sync role",assignableSubjectTypes:["user"],dataSteward:"roles@example.test"}],permissions:[{id:"ecv-sync-permission",name:"ECV Sync Permission",description:"ECV sync permission",assignableSubjectTypes:["user"],dataSteward:"permissions@example.test"}]}));e(ze.success===!0,"full sync should report success from the receiver");let De=(await Le().oaa.pushes()).at(-1);e(De!==void 0,"expected a recorded OAA sync push"),e(De.isIncremental===!1,"a full snapshot sync must not be marked incremental");let Ve=Vt(De.payload);e(Ve!==void 0,"sync payload must contain an application");let Je=ht(Ve?.local_users);for(let[Fe,He]of[["ecv-sync-user","users@example.test"],["ecv-sync-customer","customers@example.test"],["ecv-sync-workload","workloads@example.test"]]){let Xe=Je.find((Ye)=>Ye.unique_id===Fe);e(Xe?.custom_properties?.dataSteward===He,`full sync projects ${Fe} flat custom attributes`)}let Ge=ht(Ve?.local_groups).find((Fe)=>Fe.unique_id==="ecv-sync-group"),We=ht(Ve?.local_roles).find((Fe)=>Fe.unique_id==="ecv-sync-role"),Ke=ht(De.payload.permissions).find((Fe)=>Fe.unique_id==="ecv-sync-permission");e(Ge?.custom_properties?.dataSteward==="groups@example.test","full sync projects Group flat custom attributes"),e(We?.custom_properties?.dataSteward==="roles@example.test","full sync projects Role flat custom attributes"),e(Ke?.custom_properties?.dataSteward==="permissions@example.test","full sync projects Permission flat custom attributes")}},{name:"ion.authz.oaa.sync surfaces config trusted provisioning clients with eci-trusted provenance",fn:async()=>{await Le().oaa.clear();let ze={issuer:"https://idp.example/realms/ops",audience:["ecv-app"],clientId:"ecv-provisioner",permissions:[sn.id]};await $s(Le(),Ne,async(Fe)=>Fe.authz.oaa.sync({}),[ze]);let De=(await Le().oaa.pushes()).at(-1);e(De!==void 0,"expected a recorded OAA sync push"),e(De.isIncremental===!1,"trusted-client union rides the full snapshot, not an incremental push");let Ve=Vt(De.payload),Je=ht(Ve?.local_users).find((Fe)=>Fe.unique_id==="ecv-provisioner");e(Je!==void 0,"the trusted provisioning client appears as a local_user");let Ge=Je?.custom_properties??{};e(Ge.source==="eci-trusted","the trusted client carries eci-trusted provenance");let Ke=ht(De.payload.identity_to_permissions).find((Fe)=>Fe.identity==="ecv-provisioner");e(Ke!==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 Le().oaa.clear();let ze=await gt(Le().baseUrl,Qt.id),De=await wt(),Ve=new it,Je=new lt;await kt(Ve,Je,De,{subject:"ecv-reporting-workload",subjectType:"workload",targetType:"group",target:"region_north_librarian"});let Ge=(await Ve.listByTarget("group","region_north_librarian")).items,We=ot(en(Le(),"ecv-stored-workload-projection"),{validators:rt(),iam:{inboundWorkloads:{mapBaseWorkloadToScim:(Ke)=>({schemas:["urn:ionite:scim:schemas:core:2.0:Workload"],id:Ke.id,externalId:Ke.externalId,userName:Ke.clientId??Ke.id,displayName:Ke.displayName??Ke.id,active:Ke.active??!0,tenantId:Ke.tenantId})}},stores:{catalogStore:De.catalog,accessStore:Ve,accessIndex:Je,resources:De.resources}});try{await We.ready(Ne);let Ke=Ge[0];e(Ke!==void 0,"expected the stored workload grant root"),await We.authz.oaa.grant(Ke);let Fe=(await Le().oaa.pushes()).at(-1);e(Fe!==void 0,"expected a recorded incremental OAA grant push"),e(Fe.isIncremental===!0,"the workload grant uses an incremental payload");let He=ht(Vt(Fe.payload)?.local_users).find((et)=>et.unique_id==="ecv-reporting-workload");e(He?.custom_properties?.identity_type==="workload","the incremental local_user carries identity_type=workload"),await Le().oaa.clear(),await We.authz.oaa.sync({workloads:[{id:"ecv-reporting-workload",clientId:"ecv-reporting-workload",tenantId:"ecv-preserved-tenant"}],access:Ge});let Xe=(await Le().oaa.pushes()).at(-1);e(Xe!==void 0,"expected a recorded OAA sync push");let Ye=ht(Vt(Xe.payload)?.local_users).find((et)=>et.unique_id==="ecv-reporting-workload");e(Ye!==void 0,"the stored workload grant projects its subject as a local_user");let Ze=Ye?.custom_properties??{};e(Ze.identity_type==="workload","the local_user carries identity_type=workload"),e(Ze.tenantId==="ecv-preserved-tenant","the workload stamp preserves existing local_user custom properties"),await kt(Ve,Je,De,{subject:"ecv-reporting-workload",subjectType:"user",targetType:"group",target:"region_north_librarian"});let tt=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/region_north_librarian",{headers:{Authorization:`Bearer ${ze}`}}));e(tt.status===200,`requestable Group read should succeed, got ${tt.status}`);let Qe=await tt.json(),st=Qe[Pt]??{};e(st.requestable===!0,"the SCIM group is requestable"),e(Qe.members?.some((et)=>et.value==="ecv-reporting-workload"&&et.type==="Workload")===!0,"the stored workload grant appears as a SCIM member with type Workload"),e(Qe.members?.some((et)=>et.value==="ecv-reporting-workload"&&et.type==="User")===!0,"a same-id user grant remains a distinct SCIM member")}finally{We.close?.()}}},{name:"OAA customer population keeps access subjects as a floor and all widens enumeration",fn:async()=>{let ze=[{id:"ecv-customer-with-access",externalId:"customer-with-access",userName:"customer-with-access",name:"Customer With Access",email:"with-access@example.test",displayName:"Customer With Access",loyaltyTier:"gold"},{id:"ecv-customer-without-access",externalId:"customer-without-access",userName:"customer-without-access",name:"Customer Without Access",email:"without-access@example.test",displayName:"Customer Without Access",loyaltyTier:"silver"}],De=[{subject:"ecv-customer-with-access",subjectType:"customer",targetType:"permission",target:"ecv-customer-read"}],Ve={validators:rt(),iam:{inboundCustomers:{mapBaseCustomerToScim:(We)=>({schemas:["urn:ionite:scim:schemas:core:2.0:Customer"],id:We.id,externalId:We.externalId,displayName:We.displayName,active:We.active??!0,loyaltyTier:We.loyaltyTier})}}};await Le().oaa.clear();let Je=ot(en(Le(),"ecv-customer-access-population"),Ve);try{await Je.ready(Ne);let We=De[0];e(We!==void 0,"expected the customer access root"),await Je.authz.oaa.grant(We);let Ke=(await Le().oaa.pushes()).at(-1),Fe=ht(Vt(Ke?.payload??{})?.local_users).find((Ze)=>Ze.unique_id==="ecv-customer-with-access");e(Ke?.isIncremental===!0,"the customer grant uses an incremental payload"),e(Fe?.custom_properties?.identity_type==="customer","the incremental customer access floor carries identity_type=customer"),await Le().oaa.clear(),await Je.authz.oaa.sync({customers:ze,access:De});let He=(await Le().oaa.pushes()).at(-1)?.payload,Xe=ht(Vt(He??{})?.local_users),Ye=Xe.find((Ze)=>Ze.unique_id==="ecv-customer-with-access");e(Ye!==void 0,"an access-holding customer is projected under the default access mode"),e(Ye?.custom_properties?.identity_type==="customer","the customer access floor carries identity_type=customer"),e(Ye?.custom_properties?.loyaltyTier==="gold","the AppDev customer projection reaches OAA custom properties"),e(!Xe.some((Ze)=>Ze.unique_id==="ecv-customer-without-access"),"an edge-less customer is omitted under access population")}finally{Je.close?.()}await Le().oaa.clear();let Ge=ot(en(Le(),"ecv-customer-all-population",{customers:{population:"all"}}),Ve);try{await Ge.ready(Ne),await Ge.authz.oaa.sync({customers:ze,access:[]});let We=(await Le().oaa.pushes()).at(-1)?.payload,Ke=ht(Vt(We??{})?.local_users);e(Ke.some((Fe)=>Fe.unique_id==="ecv-customer-without-access"),"population=all includes edge-less customers")}finally{Ge.close?.()}}},{name:"incremental group push is referentially closed (conferred role + its permission definitions ride along)",fn:async()=>{await Le().oaa.clear();let ze=new St;await ze.permissions.set({id:"ecv-status",name:"System Status Checks",description:"status",assignableSubjectTypes:["user"]}).ready(),await ze.roles.set({id:"ecv-admin",name:"Admin",description:"admin",permissions:["ecv-status"],assignableSubjectTypes:["user"]}).ready();let De={id:"ecv-supers",name:"Super Admins",description:"closure test",assignableSubjectTypes:["user"],roles:["ecv-admin"]};await ze.groups.set(De).ready(),await Le().oaa.clear(),await Uo(Le(),Ne,ze,async(Fe)=>{await Fe.authz.oaa.pushGroup(De)});let Ve=(await Le().oaa.pushes()).at(-1);e(Ve!==void 0,"expected a recorded OAA group push");let Je=Vt(Ve.payload),Ge=ht(Je?.local_roles);e(Ge.some((Fe)=>Fe.unique_id==="ecv-admin"),"the conferred role definition must ride along so the reference is not dangling");let Ke=ht(Ve.payload.permissions).find((Fe)=>Fe.unique_id==="ecv-status");e(Ke!==void 0,"the permission the conferred role confers must also be included"),e(Ke?.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 Le().oaa.clear();let ze={id:"team:acme",type:"team",name:"Acme",description:"Acme team",resources:["project:phoenix"]},De={id:"project:phoenix",type:"project",name:"Phoenix",description:"Phoenix project",parent:"team:acme"};await $s(Le(),Ne,async(Fe)=>{await Fe.authz.oaa.sync({resources:[ze,De]})});let Ve=(await Le().oaa.pushes()).at(-1);e(Ve!==void 0,"expected a recorded OAA sync push"),e(Ve.isIncremental===!1,"full sync must not be marked incremental");let Je=Vt(Ve.payload),Ge=ht(Je?.resources);e(Ge.some((Fe)=>Fe.id==="team:acme"),"the team resource is a top-level OAA resource"),e(!Ge.some((Fe)=>Fe.id==="project:phoenix"),"the child project must not also appear as a top-level sibling");let We=Ge.find((Fe)=>Fe.id==="team:acme"),Ke=ht(We?.sub_resources);e(Ke.some((Fe)=>Fe.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 Le().oaa.clear();let ze=new St,De=new Mo,Ve={id:"team:acme",type:"team",name:"Acme",description:"Acme team",resources:["project:phoenix"]},Je={id:"project:phoenix",type:"project",name:"Phoenix",description:"Phoenix project",parent:"team:acme"};De.set(Ve),De.set(Je),await Le().oaa.clear(),await Uo(Le(),Ne,ze,async(Xe)=>{await Xe.authz.oaa.pushResource(Je)},De);let Ge=(await Le().oaa.pushes()).at(-1);e(Ge!==void 0,"expected a recorded OAA resource push"),e(Ge.isIncremental===!0,"pushResource is an incremental push");let We=Vt(Ge.payload),Ke=ht(We?.resources);e(Ke.some((Xe)=>Xe.id==="team:acme"),"incremental push walks parent to the top-level ancestor"),e(!Ke.some((Xe)=>Xe.id==="project:phoenix"),"the child is nested, not duplicated at top level");let Fe=Ke.find((Xe)=>Xe.id==="team:acme"),He=ht(Fe?.sub_resources);e(He.some((Xe)=>Xe.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 Le().oaa.clear(),await $s(Le(),Ne,async(Ve)=>{await Ve.authz.oaa.deleteUser("ecv-del-user"),await Ve.authz.oaa.deleteGroup("ecv-del-group"),await Ve.authz.oaa.deleteRole("ecv-del-role"),await Ve.authz.oaa.deletePermission("ecv-del-perm"),await Ve.authz.oaa.deleteResource("ecv-del-resource")});let ze=await Le().oaa.pushes();e(ze.length>=5,"each entity delete should produce a push");let De=(Ve,Je,Ge)=>ze.some((We)=>{let Ke=Vt(We.payload)??{};return Ve(Ke,We.payload).some((Fe)=>Fe[Je]===Ge&&Fe.operation==="delete")});e(De((Ve)=>ht(Ve.local_users),"unique_id","ecv-del-user"),"deleteUser marks the user with operation:delete"),e(De((Ve)=>ht(Ve.local_groups),"unique_id","ecv-del-group"),"deleteGroup marks the group with operation:delete"),e(De((Ve)=>ht(Ve.local_roles),"unique_id","ecv-del-role"),"deleteRole marks the role with operation:delete"),e(De((Ve,Je)=>ht(Je.permissions),"unique_id","ecv-del-perm"),"deletePermission marks the permission with operation:delete"),e(De((Ve)=>ht(Ve.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 ze=ot(Qa(Le(),!1),{validators:rt(),stores:{catalogStore:new St}});try{await ze.ready(Ne);let De=await ze.handler(new Request(Ea,{method:"POST"}));e(De.status===404,`disabled full-sync endpoint should 404, got ${De.status}`)}finally{ze.close?.()}}},{name:"OAA full-sync endpoint requires workload auth when enabled (401 without a token)",fn:async()=>{let ze=ot(Qa(Le(),!0),{validators:rt(),stores:{catalogStore:new St}});try{await ze.ready(Ne);let De=await ze.handler(new Request(Ea,{method:"POST"}));e(De.status!==404,"an enabled endpoint must not 404"),e(De.status!==200,"an unauthenticated caller must not be allowed to trigger a sync"),e(De.status===401||De.status===403,`unauthenticated full-sync should be denied (401/403), got ${De.status}`)}finally{ze.close?.()}}},{name:"OAA full-sync endpoint runs a snapshot for an authorized workload and returns 405 for non-POST",fn:async()=>{await Le().oaa.clear();let ze=await gt(Le().baseUrl,jo.id),De=ot(Qa(Le(),!0),{validators:rt(),stores:{catalogStore:new St}});try{await De.ready(Ne);let Ve=await De.handler(new Request(Ea,{method:"GET"}));e(Ve.status===405,`full-sync endpoint is POST-only, got ${Ve.status} for GET`);let Je=await De.handler(new Request(Ea,{method:"POST",headers:{Authorization:`Bearer ${ze}`}}));e(Je.status===200,`authorized full-sync should succeed, got ${Je.status}`);let Ge=await Je.json();e(Ge.reconcile!==void 0&&typeof Ge.reconcile.subjects==="number"&&typeof Ge.reconcile.roots==="number"&&typeof Ge.reconcile.cleared==="number","the endpoint bundles a full-reconcile report alongside the OAA sync result");let We=(await Le().oaa.pushes()).at(-1);e(We!==void 0,"the endpoint should drive an OAA sync push"),e(We.isIncremental===!1,"the endpoint runs a full (snapshot) sync, not an incremental push")}finally{De.close?.()}}},{name:"routing.resolve replacing iam.users inherits the requiredPermission gate unless it opts out with null (§16.1)",fn:async()=>{let ze=await wt(),De="custom-iam-users-handler",Ve=ot(mt({id:"ecv-resolve-inherit",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}}),{validators:rt(),stores:{catalogStore:ze.catalog,accessStore:new it,accessIndex:new lt,resources:ze.resources},routing:{resolve:(Je,Ge,We)=>{if(!We||new URL(Je.url).pathname!=="/api/ionite/iam/Users")return We;let Ke={module:We.module,kind:"iam.users.custom",handler:async()=>new Response("custom-iam-users-handler",{status:200})};if(Je.headers.get("x-ion-resolve-mode")==="public")return{...Ke,requiredPermission:null};return Ke}}});try{await Ve.ready(Ne);let Je=await Ve.handler(new Request("https://ecv.test/api/ionite/iam/Users",{headers:{"x-ion-resolve-mode":"inherit"}}));e(Je.status===401||Je.status===403,`a replaced route omitting requiredPermission must inherit the gate (401/403), got ${Je.status}`),e(await Je.text()!=="custom-iam-users-handler","the custom handler must not run when the inherited gate denies");let Ge=await Ve.handler(new Request("https://ecv.test/api/ionite/iam/Users",{headers:{"x-ion-resolve-mode":"public"}}));e(Ge.status===200,`requiredPermission: null must reach the handler, got ${Ge.status}`),e(await Ge.text()==="custom-iam-users-handler","the explicit public opt-out reaches the custom handler");let We=await Ve.handler(new Request("https://ecv.test/api/ionite/does-not-exist"));e(We.status===404,`an unmatched route returns 404, got ${We.status}`);let Ke=await We.json();t(Ke.error,"not_found",'the router 404 uses the unified JSON shape { error: "not_found" }')}finally{Ve.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 ze=await gt(Le().baseUrl,Gt.id),De=await wt(),Ve=new it,Je=new qn,Ge=ot(mt({id:"ecv-scim-repair",workload:{jwksUri:`${Le().baseUrl}/workload/certs`,audience:`${Le().baseUrl}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}}),{validators:rt(),stores:{catalogStore:De.catalog,accessStore:Ve,accessIndex:Je,resources:De.resources}});try{await Ge.ready(Ne);let We=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"repair-user"}]}]})}));e(We.status>=400,`a failed access mutation should return a SCIM error, got ${We.status}`),e((await Ve.listBySubject("repair-user","user")).items.length===1,"the grant root is durably committed even though the index write failed"),e((await Je.hasGroup("repair-user","staff",void 0,"user")).allow,"the SDK awaited-repair re-materializes the subject index within the request (no IDP retry)"),e((await Je.hasPermission("repair-user","read",void 0,"user")).allow,"the repaired closure includes the role/permission edges (editor -> read via staff)")}finally{Ge.close?.()}}},{name:"settlement: onAccessMutation fires exactly once with summary.egress on every exit path",fn:async()=>{let ze=Le().baseUrl,De=await gt(ze,Gt.id),Ve=await gt(ze,Wr.id),Je={jwksUri:`${ze}/workload/certs`,audience:`${ze}/workload`,claimsAsEntitlements:{claims:["scope"]}},Ge=(We,Ke)=>new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${Ke}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:We}]}]})});{let We=await wt(),Ke=[],Fe=ot(mt({id:"settle-success",workload:Je,iam:{}}),{validators:rt(),authz:{onAccessMutation:(He)=>Ke.push(He)},stores:{catalogStore:We.catalog,accessStore:new it,accessIndex:new lt,resources:We.resources}});try{await Fe.ready(Ne);let He=await Fe.handler(Ge("settle-ok",De));e(He.status>=200&&He.status<300,`success exit should be 2xx, got ${He.status}`),t(Ke.length,1,"onAccessMutation fires exactly once on the success exit"),e(Ke[0]?.egress!==void 0,"summary.egress is populated on the success exit"),e(Ke[0]?.access.some((Xe)=>Xe.status==="applied"),"success summary records the applied grant")}finally{Fe.close?.()}}{let We=await wt(),Ke=[],Fe=ot(mt({id:"settle-access-fail",workload:Je,iam:{}}),{validators:rt(),authz:{onAccessMutation:(He)=>Ke.push(He)},stores:{catalogStore:We.catalog,accessStore:new it,accessIndex:new qn,resources:We.resources}});try{await Fe.ready(Ne);let He=await Fe.handler(Ge("settle-fail",De));e(He.status>=400,`access-failure exit should return a SCIM error, got ${He.status}`),t(Ke.length,1,"onAccessMutation fires exactly once on the access-failure exit"),e(Ke[0]?.egress!==void 0,"summary.egress is populated on the access-failure exit"),e(Ke[0]?.access.some((Xe)=>Xe.status==="failed"),"access-failure summary records the failed access mutation")}finally{Fe.close?.()}}{let We=[],Ke=ot(mt({id:"settle-metadata-fail",workload:Je,iam:{}}),{validators:rt(),authz:{onAccessMutation:(Fe)=>We.push(Fe)},stores:{userStore:new Fr}});try{await Ke.ready(Ne);let Fe=await Ke.handler(new Request("https://ecv.test/api/ionite/iam/Users",{method:"POST",headers:{Authorization:`Bearer ${Ve}`,...Nt},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}]})}));e(Fe.status>=400,`metadata-failure exit should return a SCIM error, got ${Fe.status}`),t(We.length,1,"onAccessMutation fires exactly once on the metadata-failure exit"),e(We[0]?.egress!==void 0,"summary.egress is populated on the metadata-failure exit"),e(We[0]?.metadataApplied===!1,"metadata-failure summary marks metadataApplied false")}finally{Ke.close?.()}}}},{name:"settlement: a degraded OAA push is non-fatal (SCIM stays 2xx, summary.egress.oaa is degraded)",fn:async()=>{let ze=Le(),De=ze.baseUrl,Ve=await gt(De,Gt.id),Je=await wt(),Ge=[],We=ot(mt({id:"settle-oaa-degraded",workload:{jwksUri:`${De}/workload/certs`,audience:`${De}/workload`,claimsAsEntitlements:{claims:["scope"]}},authz:{oaa:{url:`${De}/__oaa_missing__`,apiKey:ze.oaa.apiKey,providerId:ze.oaa.providerId,datasourceId:ze.oaa.datasourceId,applicationName:"ECV Settlement Degraded"}},iam:{}}),{validators:rt(),authz:{onAccessMutation:(Ke)=>Ge.push(Ke)},stores:{catalogStore:Je.catalog,accessStore:new it,accessIndex:new lt,resources:Je.resources}});try{await We.ready(Ne),e(We.authz.oaa.isReady(),"OAA must be configured/ready so the push is actually attempted");let Ke=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:{Authorization:`Bearer ${Ve}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"oaa-degraded-member"}]}]})}));e(Ke.status>=200&&Ke.status<300,`a degraded OAA push must not fail the SCIM response, got ${Ke.status}`),t(Ge.length,1,"onAccessMutation fires exactly once"),t(Ge[0]?.egress?.oaa.status,"degraded","the failed OAA push is recorded as degraded egress"),e(Ge[0]?.access.some((Fe)=>Fe.status==="applied"),"the underlying store mutation still committed (applied)")}finally{We.close?.()}}},{name:"SCIM Groups exposes only requestable app rows across list/get/externalId and write paths",fn:async()=>{let ze=await gt(Le().baseUrl,Qt.id),De=await gt(Le().baseUrl,Gt.id),Ve=await wt();await Ve.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 Je=tn(),Ge=ot(Ss(Le().baseUrl,"ecv-app-row-read"),{validators:rt(),stores:{catalogStore:Ve.catalog,accessStore:new it,accessIndex:new lt,resources:Je}});try{await Ge.ready(Ne);let We={Authorization:`Bearer ${ze}`},Ke=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{headers:We}));e(Ke.status===200,`Groups list should succeed, got ${Ke.status}`);let Fe=await Ke.json(),He=(Fe.Resources??[]).map((Rt)=>Rt.id);e(He.includes("acme_team_admin"),"the durable app-authored entitlement is listed"),e(!He.includes("internal_staff"),"the non-requestable stored group is hidden"),e(Fe.totalResults===He.length,"totalResults counts only the requestable IAM surface");let Ze=((await(await Ge.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq true")}`,{headers:We}))).json()).Resources??[]).map((Rt)=>Rt.id);e(Ze.includes("acme_team_admin"),"the app entitlement passes the requestable filter"),e(!Ze.includes("internal_staff"),"the non-requestable stored group is excluded");let Qe=await(await Ge.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq false")}`,{headers:We}))).json();e(Qe.totalResults===0&&(Qe.Resources??[]).length===0,"requestable eq false cannot reveal internal definitions");let et=await(await Ge.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "iam-acme-team-admin"')}`,{headers:We}))).json();e((et.Resources??[]).some((Rt)=>Rt.id==="acme_team_admin"),"externalId filtering matches the row by providerId");let dt=await(await Ge.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "iam-internal-staff"')}`,{headers:We}))).json();e(dt.totalResults===0&&(dt.Resources??[]).length===0,"externalId lookup cannot reveal a non-requestable row");let pt=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{headers:We}));e(pt.status===200,`app Group GET should succeed, got ${pt.status}`);let ct=await pt.json();e(ct.id==="acme_team_admin"&&ct.displayName==="Acme Admin","durable presentation is projected"),e(ct[Pt]?.source==="app","the SCIM projection carries source:app provenance"),e(ct[Pt]?.resources?.[0]==="team:acme","the SCIM projection carries the durable resource binding");let ft=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/internal_staff",{headers:We}));e(ft.status===404,`non-requestable point GET must be 404, got ${ft.status}`);let ut=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/internal_staff",{method:"PATCH",headers:{Authorization:`Bearer ${De}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"must-not-grant"}]}]})}));e(ut.status===404,`IAM membership write on an internal entitlement must be 404, got ${ut.status}`)}finally{Ge.close?.()}}},{name:"a member grant on an app catalog row materializes resource-scoped access and IAM receipt write-back is accepted",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=await wt();await De.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","workload"],roles:["editor"],resources:["team:acme"],source:"app"}).ready();let Ve=tn(),Je=new it,Ge=new lt,We=ot(Ss(Le().baseUrl,"ecv-app-row-write"),{validators:rt(),stores:{catalogStore:De.catalog,accessStore:Je,accessIndex:Ge,resources:Ve}});try{await We.ready(Ne);let Ke={Authorization:`Bearer ${ze}`,...Nt},Fe=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:Ke,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"}]}]})}));e(Fe.status<300,`member add on an app entitlement should succeed, got ${Fe.status}`),e((await Ge.hasRole("derived-member","editor","team:acme","user")).allow,"the grant confers the app-authored role scoped to the backing resource"),e(!(await Ge.hasRole("derived-member","editor","region:north","user")).allow,"the app-authored scope does not leak to unrelated resources");let He=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:Ke,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.providerId`,value:"iam-group-42"},{op:"replace",path:`${Pt}.requestUrl`,value:"https://iam.example.test/request/42"}]})}));e(He.status<300,`IAM receipt PATCH should succeed, got ${He.status}`);let Xe=await De.catalog.groups.get("acme_team_admin");e(Xe?.providerId==="iam-group-42","IAM providerId is written back to the app row"),e(Xe?.requestUrl==="https://iam.example.test/request/42","IAM requestUrl is written back to the app row")}finally{We.close?.()}}},{name:"code templates union publicly, materialize on SCIM discovery, and preserve IAM-owned presentation",fn:async()=>{let ze=await gt(Le().baseUrl,Qt.id),De=await gt(Le().baseUrl,Gt.id),Ve=await wt(),Je=new lt;await Ve.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",assignableSubjectTypes:["user"],roles:["stored-role-must-not-control-structure"]}).ready();let Ge=ot(Ss(Le().baseUrl,"ecv-code-read"),{validators:rt(),stores:{catalog:Ws(),catalogStore:Ve.catalog,accessStore:new it,accessIndex:Je}});try{await Ge.ready(Ne);let We={Authorization:`Bearer ${ze}`},Ke=await Ge.authz.entitlements.list(),Fe=Ke.items.map((ut)=>ut.id);e(Fe.includes("code_staff"),"the public API includes a code id with a materialized row"),e(Fe.includes("code_editor"),"the public API includes unmaterialized code templates"),e(Fe.includes("staff"),"the public API unions app/store rows"),e(Ke.items.find((ut)=>ut.id==="code_read")?.riskLevel==="organization-critical","defineCatalog accepts a non-canonical open-string riskLevel"),e(await Ve.catalog.roles.get("code_editor")===void 0,"public union reads do not themselves materialize code templates");let He=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{headers:We}));e(He.status===200,`Groups list should succeed, got ${He.status}`);let Xe=await He.json(),Ye=(Xe.Resources??[]).map((ut)=>ut.id);e(Ye.includes("code_staff"),"the code-declared group is listed"),e(Xe.Resources?.find((ut)=>ut.id==="code_sensitive_reports")?.[Pt]?.riskLevel==="organization-critical","an AppDev-authored non-canonical riskLevel appears in SCIM Groups pull"),e(Xe.Resources?.find((ut)=>ut.id==="code_sensitive_reports")?.[Pt]?.attributes?.dataSteward==="audit@example.test","flat AppDev attributes appear in the same SCIM Group projection as riskLevel"),e(!Ye.includes("code_editor"),"the non-requestable code role is hidden from IAM"),e(Ye.includes("staff"),"the requestable store group remains visible to IAM"),e(!Ye.includes("internal_staff"),"the non-requestable store group is hidden from IAM"),e(Xe.totalResults===Ye.length,"totalResults counts only requestable entries"),e((await Ve.catalog.groups.get("code_staff"))?.source==="code","SCIM Group list lazily materializes requestable code templates"),e(await Ve.catalog.roles.get("code_editor")===void 0&&await Ve.catalog.permissions.get("code_read")===void 0,"SCIM Group list does not materialize internal code templates");let Qe=((await(await Ge.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent("requestable eq true")}`,{headers:We}))).json()).Resources??[]).map((ut)=>ut.id);e(Qe.includes("code_staff"),"the code template requestability wins over the stale stored presentation"),e(Qe.includes("staff"),"the requestable store group remains included"),e(!Qe.includes("internal_staff"),"the non-requestable store group is excluded");let nt=((await(await Ge.handler(new Request(`https://ecv.test/api/ionite/iam/Groups?filter=${encodeURIComponent('externalId eq "code_editor"')}`,{headers:We}))).json()).Resources??[]).find((ut)=>ut.id==="code_editor");e(nt===void 0,"externalId filtering hides the internal code entitlement"),e(await Ve.catalog.roles.get("code_editor")===void 0,"hidden externalId lookup does not materialize the internal code entitlement");let dt=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{headers:We}));e(dt.status===200,`code Group GET should succeed, got ${dt.status}`);let pt=await dt.json();e(pt.id==="code_staff"&&pt.externalId==="provider-code-staff","the App id and IAM provider id are projected in their SCIM id/externalId slots"),e(pt.displayName==="IAM Renamed Staff","the existing row owns presentation"),e(pt[Pt]?.requestable===!0,"code-owned requestability overrides the stale stored value"),e(pt[Pt]?.riskLevel==="basic","an entitlement without an authored or stored riskLevel projects the basic default"),e(pt[Pt]?.providerId==="provider-code-staff"&&pt[Pt]?.requestUrl==="https://iam.example.test/request/code-staff","the existing row owns IAM receipts");let ct=await Ve.catalog.groups.get("code_staff");e(ct?.displayName==="IAM Renamed Staff"&&ct.roles?.[0]==="stored-role-must-not-control-structure","insert-if-absent discovery never overwrites an existing durable row");let ft=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:{Authorization:`Bearer ${De}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"iam-presentation-member"}]}]})}));e(ft.status<300,`membership grant should succeed, got ${ft.status}`),e((await Je.hasRole("iam-presentation-member","code_editor",void 0,"user")).allow,"code structural references remain authoritative over conflicting stored structure"),e(!(await Je.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{Ge.close?.()}}},{name:"first code membership grant materializes durably; IAM presentation writes succeed while structure stays code-owned",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new it,Ve=new lt,Je=new St,Ge=ot(Ss(Le().baseUrl,"ecv-code-write"),{validators:rt(),stores:{catalog:Ws(),catalogStore:Je,accessStore:De,accessIndex:Ve}});try{await Ge.ready(Ne);let We={Authorization:`Bearer ${ze}`,...Nt};e(await Je.groups.get("code_staff")===void 0,"the code template begins unmaterialized");let Ke=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"code-member"}]}]})}));e(Ke.status<300,`member add on a code entitlement should succeed, got ${Ke.status}`);let Fe=await Je.groups.get("code_staff");e(Fe?.source==="code","the first membership grant materializes source:code"),e(Fe?.roles?.[0]==="code_editor","the durable candidate initially carries code structure"),e(Fe?.dataSteward===void 0,"materialization does not snapshot the AppDev custom-attribute recommendation"),e((await Ve.hasRole("code-member","code_editor",void 0,"user")).allow,"the grant confers the code role"),e((await Ve.hasPermission("code-member","code_read",void 0,"user")).allow,"the code role confers its permissions");let He={count:0},Xe=()=>He.count,Ye=Je.afterChange?.(()=>{He.count+=1}),Ze=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.riskLevel`,value:"basic"}]})}));e(Ze.status<300,`default riskLevel PATCH should succeed, got ${Ze.status}`),e((await Je.groups.get("code_staff"))?.riskLevel===void 0,"writing the projected basic default onto an unset row does not persist an explicit value"),e(Xe()===0,"writing basic onto an unset row is a value-identical no-op");let tt=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"remove",path:'members[value eq "code-member"]'}]})}));e(tt.status<300,`member remove should succeed, got ${tt.status}`),e(!(await Ve.hasGroup("code-member","code_staff",void 0,"user")).allow,"the revoke converges the closure");let Qe=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,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:`${Pt}.providerId`,value:"iam-code-staff"},{op:"replace",path:`${Pt}.requestUrl`,value:"https://iam.example.test/request/code-staff"},{op:"replace",path:`${Pt}.riskLevel`,value:"iam-custom-critical"},{op:"replace",path:`${Pt}.attributes.dataSteward`,value:"iam-governance@example.test"}]})}));e(Qe.status<300,`code presentation/receipt PATCH should succeed, got ${Qe.status}`);let st=await Je.groups.get("code_staff");e(st?.displayName==="IAM Renamed Code Staff","IAM presentation persists on the row"),e(st?.providerId==="iam-code-staff"&&st.requestUrl==="https://iam.example.test/request/code-staff","IAM receipt fields persist on the row"),e(st?.riskLevel==="iam-custom-critical","IAM riskLevel presentation persists on the row"),e(st?.dataSteward==="iam-governance@example.test","IAM custom-attribute presentation persists flat on the row"),e(Xe()===1,"a changed riskLevel emits one substantive catalog change"),e(st?.roles?.[0]==="code_editor","presentation write-back does not erase code structure"),e((await Ge.authz.entitlements.list()).items.find((ut)=>ut.id==="code_staff")?.riskLevel==="iam-custom-critical","the runtime catalog union returns the IAM-owned riskLevel over the code template"),e((await Ge.authz.entitlements.list()).items.find((ut)=>ut.id==="code_staff").dataSteward==="iam-governance@example.test","the runtime catalog union returns the IAM custom-attribute override over the code recommendation");let et=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.attributes.dataSteward`,value:"identity-governance@example.test"}]})}));e(et.status<300,`custom attribute echo PATCH should succeed, got ${et.status}`),e((await Je.groups.get("code_staff"))?.dataSteward===void 0,"writing the AppDev seed clears the durable IAM custom-attribute override"),e(Xe()===2,"clearing a custom-attribute override emits one substantive catalog change"),Ye?.();let nt=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.requestable`,value:!1}]})}));e(nt.status===403,`code-owned requestability PATCH must be 403, got ${nt.status}`);let dt=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.resources`,value:["team:hijack"]}]})}));e(dt.status===403,`code structure PATCH must be 403, got ${dt.status}`);let pt=await dt.json();e(pt.scimType==="mutability","the rejection is a SCIM mutability error");let ct=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"PUT",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],id:"code_staff",externalId:"iam-code-staff-v2",displayName:"Code Staff"})}));e(ct.status<300,`provider externalId PUT should succeed, got ${ct.status}`),e((await Je.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 ft=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_staff",{method:"DELETE",headers:We}));e(ft.status===403,`active code Group DELETE must be 403, got ${ft.status}`)}finally{Ge.close?.()}}},{name:"code riskLevel edits propagate after materialization; only a genuine IAM override persists",fn:async()=>{let ze=await gt(Le().baseUrl,Qt.id),De=await gt(Le().baseUrl,Gt.id),Ve={Authorization:`Bearer ${ze}`},Je={Authorization:`Bearer ${De}`,...Nt},Ge=new St,We=(Ze)=>ma({groups:[{id:"code_ops",name:"Ops",description:"Ops (code-declared)",requestable:!0,riskLevel:Ze,assignableSubjectTypes:["user"]}]}),Ke=(Ze,tt)=>ot(Ss(Le().baseUrl,Ze),{validators:rt(),stores:{catalog:We(tt),catalogStore:Ge,accessStore:new it,accessIndex:new lt}}),Fe=async(Ze)=>{let tt=await Ze.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_ops",{headers:Ve}));return e(tt.status===200,`code_ops GET should succeed, got ${tt.status}`),(await tt.json())[Pt]?.riskLevel},He=async(Ze,tt)=>{let Qe=await Ze.handler(new Request("https://ecv.test/api/ionite/iam/Groups/code_ops",{method:"PATCH",headers:Je,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.riskLevel`,value:tt}]})}));e(Qe.status<300,`riskLevel PATCH should succeed, got ${Qe.status}`)},Xe=Ke("ecv-code-risk-v1","elevated");try{await Xe.ready(Ne),e(await Fe(Xe)==="elevated","the code-seeded riskLevel projects on pull");let Ze=await Ge.groups.get("code_ops");e(Ze?.source==="code"&&Ze.riskLevel===void 0,"materialization persists structure, never the advisory riskLevel seed")}finally{Xe.close?.()}let Ye=Ke("ecv-code-risk-v2","admin");try{await Ye.ready(Ne),e(await Fe(Ye)==="admin","a code riskLevel edit propagates through the materialized row"),await He(Ye,"iam-custom-critical"),e((await Ge.groups.get("code_ops"))?.riskLevel==="iam-custom-critical","a non-seed IAM riskLevel persists as a stored override"),e(await Fe(Ye)==="iam-custom-critical","the IAM override wins over the code seed"),await He(Ye,"admin"),e((await Ge.groups.get("code_ops"))?.riskLevel===void 0,"echoing the code seed clears the stored override instead of freezing it"),e(await Fe(Ye)==="admin","the row serves the code seed after the override is cleared")}finally{Ye.close?.()}}},{name:"SCIM create stores the client externalId as providerId without claiming the App id",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new St,Ve=ot(Ss(Le().baseUrl,"ecv-code-collision"),{validators:rt(),authz:{onInboundEntitlement:()=>({accept:!0})},stores:{catalog:Ws(),catalogStore:De,accessStore:new it,accessIndex:new lt}});try{await Ve.ready(Ne);let Je=await Ve.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"Impostor",externalId:"code_staff"})}));e(Je.status===201,`a client externalId create must succeed, got ${Je.status}`);let Ge=await Je.json();e(Ge.id!=="code_staff","the App mints a distinct catalog id"),e(Ge.externalId==="code_staff","the response echoes the client-owned externalId"),e(Ge.id!==void 0&&(await De.groups.get(Ge.id))?.providerId==="code_staff","the client externalId is stored as providerId")}finally{Ve.close?.()}}},{name:"SCIM Group create defaults to requestable and rejects explicitly internal definitions",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new St,Ve=ot(Ss(Le().baseUrl,"ecv-requestable-create"),{validators:rt(),authz:{onInboundEntitlement:()=>({accept:!0})},stores:{catalogStore:De,accessStore:new it,accessIndex:new lt}});try{await Ve.ready(Ne);let Je={Authorization:`Bearer ${ze}`,...Nt},Ge=await Ve.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:Je,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],externalId:"iam-authored-requestable",displayName:"IAM Authored Requestable"})}));e(Ge.status===201,`plain SCIM Group create should succeed, got ${Ge.status}`);let We=await Ge.json();e(We[Pt]?.requestable===!0,"a SCIM-authored Group defaults to requestable"),e((await De.groups.get(We.id??""))?.requestable===!0,"the requestable default is durable");let Ke=await Ve.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:Je,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group",Pt],externalId:"iam-authored-internal",displayName:"IAM Authored Internal",[Pt]:{requestable:!1}})}));e(Ke.status===400,`IAM cannot create an internal entitlement, got ${Ke.status}`)}finally{Ve.close?.()}}},{name:"SCIM entitlement create returns 501 without the App opt-in callback",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=ot(Ss(Le().baseUrl,"ecv-inbound-entitlement-disabled"),{validators:rt(),stores:{catalogStore:new St}});try{await De.ready(Ne);let Ve=await De.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"Not Accepted"})}));e(Ve.status===501,`create without onInboundEntitlement must return 501, got ${Ve.status}`)}finally{De.close?.()}}},{name:"SCIM entitlement create returns 403 when the App rejects it",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=ot(Ss(Le().baseUrl,"ecv-inbound-entitlement-rejected"),{validators:rt(),authz:{onInboundEntitlement:()=>({accept:!1,reason:"not in the App allow-list"})},stores:{catalogStore:new St}});try{await De.ready(Ne);let Ve=await De.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"Rejected"})}));e(Ve.status===403,`App-rejected entitlement must return 403, got ${Ve.status}`)}finally{De.close?.()}}},{name:"accepted IAM-sourced entitlement binds App structure before granting members",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=new lt,Ve=ot(Ss(Le().baseUrl,"ecv-inbound-entitlement-bound"),{validators:rt(),authz:{onInboundEntitlement:()=>({accept:!0,permissions:["code_read"],assignableSubjectTypes:["user"]})},stores:{catalog:Ws(),catalogStore:new St,accessStore:new it,accessIndex:De}});try{await Ve.ready(Ne);let Je=await Ve.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"IAM Bound Readers",members:[{value:"inbound-reader",type:"User"}]})}));e(Je.status===201,`accepted entitlement must return 201, got ${Je.status}`),e((await De.hasPermission("inbound-reader","code_read",void 0,"user")).allow,"the App-bound permission is effective for the IAM-provided member")}finally{Ve.close?.()}}},{name:"accepted IAM-sourced entitlement returns 500 for an unresolved App binding",fn:async()=>{let ze=await gt(Le().baseUrl,Gt.id),De=ot(Ss(Le().baseUrl,"ecv-inbound-entitlement-invalid-binding"),{validators:rt(),authz:{onInboundEntitlement:()=>({accept:!0,permissions:["missing_permission"]})},stores:{catalog:Ws(),catalogStore:new St}});try{await De.ready(Ne);let Ve=await De.handler(new Request("https://ecv.test/api/ionite/iam/Groups",{method:"POST",headers:{Authorization:`Bearer ${ze}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"Broken Binding"})}));e(Ve.status===500,`unresolved App binding must return 500, got ${Ve.status}`)}finally{De.close?.()}}},{name:"full reconcile deprecates retired code rows; SCIM exposes retirement and only then permits deletion",fn:async()=>{let ze=await gt(Le().baseUrl,Qt.id),De=await gt(Le().baseUrl,Gt.id),Ve=new St;await Ve.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,assignableSubjectTypes:["user"],source:"code",deprecated:!1}).ready(),await Ve.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,assignableSubjectTypes:["user"],source:"code",deprecated:!1}).ready();let Je=ot(Ss(Le().baseUrl,"ecv-code-retirement"),{validators:rt(),stores:{catalog:Ws(),catalogStore:Ve,accessStore:new it,accessIndex:new lt}});try{await Je.ready(Ne);let Ge=await Je.handler(new Request("https://ecv.test/api/ionite/iam/Groups/pull_retired_code_group",{headers:{Authorization:`Bearer ${ze}`}}));e(Ge.status===200,"SCIM GET should project a retired code row"),e((await Ve.groups.get("pull_retired_code_group"))?.deprecated===!0,"SCIM discovery lazily marks a source:code row missing from the current CodeCatalog deprecated");let We=await Je.handler(new Request("https://ecv.test/api/ionite/iam/Groups/pull_retired_code_group",{method:"PATCH",headers:{Authorization:`Bearer ${De}`,...Nt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.requestable`,value:!1}]})}));e(We.status===403,`retired source:code requestability remains immutable, got ${We.status}`);let Ke=await Je.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{method:"DELETE",headers:{Authorization:`Bearer ${De}`}}));e(Ke.status===403,"a source:code row cannot be deleted before retirement"),await Je.authz.reconcile(),e((await Ve.groups.get("retired_code_group"))?.deprecated===!0,"full reconcile marks a source:code row missing from the current CodeCatalog deprecated");let Fe=await Je.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{headers:{Authorization:`Bearer ${ze}`}}));e(Fe.status===200,`deprecated Group GET should succeed, got ${Fe.status}`);let He=await Fe.json();e(He[Pt]?.source==="code"&&He[Pt]?.deprecated===!0,"SCIM projects source:code and deprecated:true");let Xe=await Je.handler(new Request("https://ecv.test/api/ionite/iam/Groups/retired_code_group",{method:"DELETE",headers:{Authorization:`Bearer ${De}`}}));e(Xe.status===204,`a deprecated code row may be deleted, got ${Xe.status}`),e(await Ve.groups.get("retired_code_group")===void 0,"the retired row is removed")}finally{Je.close?.()}}},{name:"OAA full sync unions code templates with durable app rows and resource data",fn:async()=>{await Le().oaa.clear();let ze=await gt(Le().baseUrl,jo.id),De=new St;await De.groups.set({id:"stored-group",name:"Stored Group",description:"stored",source:"app",assignableSubjectTypes:["user"]}).ready(),await De.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:["code_editor"],resources:["team:acme"],source:"app"}).ready();let Ve=ot(Qa(Le(),!0),{validators:rt(),stores:{catalog:Ws(),catalogStore:De,resources:tn()}});try{await Ve.ready(Ne);let Je=await Ve.handler(new Request(Ea,{method:"POST",headers:{Authorization:`Bearer ${ze}`}}));e(Je.status===200,`authorized full-sync should succeed, got ${Je.status}`);let Ge=(await Le().oaa.pushes()).at(-1);e(Ge!==void 0&&Ge.isIncremental===!1,"the endpoint runs a full (snapshot) sync");let We=Vt(Ge.payload),Ke=ht(We?.local_groups),Fe=Ke.map((tt)=>tt.unique_id);e(Fe.includes("code_staff"),"code-declared groups are unioned into the projection"),e(Fe.includes("stored-group"),"store catalog groups remain in the projection"),e(Fe.includes("acme_team_admin"),"durable resource-scoped app rows are projected"),e(Ke.find((tt)=>tt.unique_id==="code_staff")?.custom_properties?.riskLevel==="basic","a code group without an explicit riskLevel projects the basic default to OAA");let He=ht(We?.local_roles),Xe=He.map((tt)=>tt.unique_id);e(Xe.includes("code_editor"),"code-declared roles are unioned into the projection"),e(He.find((tt)=>tt.unique_id==="code_editor")?.custom_properties?.riskLevel==="basic","a code role without an explicit riskLevel projects the basic default to OAA");let Ye=ht(Ge.payload.permissions),Ze=Ye.map((tt)=>tt.unique_id);e(Ze.includes("code_read"),"code-declared permissions are unioned into the projection"),e(Ye.find((tt)=>tt.unique_id==="code_read")?.custom_properties?.riskLevel==="organization-critical","the AppDev-seeded permission riskLevel projects to OAA"),e(Ye.find((tt)=>tt.unique_id==="code_write")?.custom_properties?.riskLevel==="basic","a code permission without an explicit riskLevel projects the basic default to OAA")}finally{Ve.close?.()}}},{name:"ResourceProvider.changed triggers an off-request-path reconcile and an incremental OAA push",fn:async()=>{await Le().oaa.clear();let ze=await wt(),De=new it,Ve=new lt;ze.resources.set({id:"team:a",type:"team",name:"Team A",description:"A team"}),await kt(De,Ve,ze,{subject:"lead",subjectType:"user",targetType:"permission",target:"manage",resources:["team:a"]});let Je=ot(No(Le()),{validators:rt(),stores:{catalogStore:ze.catalog,accessStore:De,accessIndex:Ve,resources:ze.resources}});try{await Je.ready(Ne),e(!(await Ve.hasPermission("lead","manage","proj:a1","user")).allow,"no child access before the change"),await Le().oaa.clear(),ze.resources.set({id:"proj:a1",type:"project",name:"Project A1",description:"A1",parent:"team:a"});let Ge=!1;for(let Ke=0;Ke<100&&!Ge;Ke++)await new Promise((Fe)=>setTimeout(Fe,10)),Ge=(await Ve.hasPermission("lead","manage","proj:a1","user")).allow;e(Ge,"the changed signal drives a reconcile that cascades manage to the new child");let We=!1;for(let Ke=0;Ke<100&&!We;Ke++)await new Promise((Fe)=>setTimeout(Fe,10)),We=(await Le().oaa.pushes()).some((Fe)=>{let He=Vt(Fe.payload),Xe=(Ye)=>Ye.flatMap((Ze)=>[Ze,...Xe(ht(Ze.sub_resources))]);return Fe.isIncremental===!0&&Xe(ht(He?.resources)).some((Ye)=>Ye.id==="proj:a1")});e(We,"the changed signal drives an incremental OAA resource push")}finally{Je.close?.()}}},{name:"OAA full sync includes ResourceProvider resources and durable app-authored catalog rows",fn:async()=>{await Le().oaa.clear();let ze=await gt(Le().baseUrl,jo.id),De=new St;await De.groups.set({id:"stored-group",name:"Stored Group",description:"stored",source:"app",assignableSubjectTypes:["user"]}).ready(),await De.groups.set({id:"acme_team_admin",providerId:"iam-acme-team-admin",name:"Acme Admin",description:"Durable app-authored team entitlement",requestable:!0,assignableSubjectTypes:["user"],resources:["team:acme"],source:"app"}).ready();let Ve=ot(Qa(Le(),!0),{validators:rt(),stores:{catalogStore:De,resources:tn()}});try{await Ve.ready(Ne);let Je=await Ve.handler(new Request(Ea,{method:"POST",headers:{Authorization:`Bearer ${ze}`}}));e(Je.status===200,`authorized full-sync should succeed, got ${Je.status}`);let Ge=(await Le().oaa.pushes()).at(-1);e(Ge!==void 0,"the endpoint should drive an OAA sync push"),e(Ge.isIncremental===!1,"the endpoint runs a full (snapshot) sync");let We=Vt(Ge.payload);e(ht(We?.resources).some((Fe)=>Fe.id==="team:acme"),"the snapshot streams resources from the ResourceProvider");let Ke=ht(We?.local_groups).map((Fe)=>Fe.unique_id);e(Ke.includes("acme_team_admin"),"durable app catalog rows are included in the projection"),e(Ke.includes("stored-group"),"stored catalog groups remain in the projection")}finally{Ve.close?.()}}},{name:"the SDK write path (SCIM access mutation + ResourceProvider.changed) emits only incremental OAA pushes, never a full snapshot",fn:async()=>{await Le().oaa.clear();let ze=await gt(Le().baseUrl,Gt.id),De=await wt(),Ve=tn(),Je=new it,Ge=new lt,We=ot(en(Le(),"ecv-scim-oaa-guard"),{validators:rt(),stores:{catalogStore:De.catalog,accessStore:Je,accessIndex:Ge,resources:Ve}});try{await We.ready(Ne),e(We.authz.oaa.isReady(),"OAA must be ready so the write path actually projects");let Ke={Authorization:`Bearer ${ze}`,...Nt};await De.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 Fe=async()=>(await Le().oaa.pushes()).every((Qe)=>Qe.isIncremental===!0),He=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:Ke,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"}]}]})}));e(He.status<300,`stored group member add should succeed, got ${He.status}`);let Xe=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:Ke,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"}]}]})}));e(Xe.status<300,`app group member add should succeed, got ${Xe.status}`),e((await Le().oaa.pushes()).length>=2,"each SCIM membership grant should project an incremental push"),e((await Le().oaa.pushes()).some((Qe)=>{let st=Vt(Qe.payload);return Qe.isIncremental===!0&&ht(st?.local_groups).some((et)=>et.unique_id==="acme_team_admin")}),"the durable app catalog write projects the Group incrementally"),e(await Fe(),"a SCIM access mutation must not trigger a full OAA snapshot sync");let Ye=await We.handler(new Request("https://ecv.test/api/ionite/iam/Groups/acme_team_admin",{method:"PATCH",headers:Ke,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:`${Pt}.riskLevel`,value:"iam-custom-critical"}]})}));e(Ye.status<300,`IAM riskLevel PATCH should succeed, got ${Ye.status}`),e((await Le().oaa.pushes()).some((Qe)=>{let st=ht(Vt(Qe.payload)?.local_groups).find((et)=>et.unique_id==="acme_team_admin");return Qe.isIncremental===!0&&st?.custom_properties?.riskLevel==="iam-custom-critical"}),"an IAM-authored riskLevel override projects to OAA incrementally"),e(await Fe(),"an IAM riskLevel mutation must not trigger a full OAA snapshot sync"),Ve.set({id:"team:beta",type:"team",name:"Beta",description:"Beta team"});let Ze=!1;for(let Qe=0;Qe<100&&!Ze;Qe++)await new Promise((st)=>setTimeout(st,10)),Ze=(await Le().oaa.pushes()).some((st)=>{let et=Vt(st.payload);return st.isIncremental===!0&&ht(et?.resources).some((nt)=>nt.id==="team:beta")});e(Ze,"a resource change should project the resource to OAA incrementally"),await new Promise((Qe)=>setTimeout(Qe,50));let tt=(await Le().oaa.pushes()).filter((Qe)=>Qe.isIncremental===!1);e(tt.length===0,`the SDK write path emitted ${tt.length} full snapshot push(es); full sync must only run via the Ops-gated endpoint`)}finally{We.close?.()}}},{name:"§7.1 pushAccessChanges batches N membership changes into ONE incremental push (O(1), not O(N))",fn:async()=>{await Le().oaa.clear();let ze=Array.from({length:100},(Ke,Fe)=>({change:{subject:`batch-user-${Fe}`,subjectType:"user",targetType:"group",target:"batch-group"},op:"add"})),De=new St;await De.groups.set({id:"batch-group",name:"Batch Group",description:"Batch projection target",assignableSubjectTypes:["user"]}).ready(),await Uo(Le(),Ne,De,async(Ke)=>{await Ke.authz.oaa.pushAccessChanges(ze)});let Ve=await Le().oaa.pushes();t(Ve.length,1,"a 100-change batch must issue exactly one OAA push, not one per change");let Je=Ve[0];e(Je!==void 0,"expected the single batched push to be recorded"),e(Je?.isIncremental===!0,"a batched membership push is incremental, never a full snapshot");let Ge=ht(Vt(Je?.payload??{})?.local_users);for(let Ke=0;Ke<100;Ke+=1){let Fe=Ge.find((He)=>He.unique_id===`batch-user-${Ke}`);e(Fe!==void 0,`subject batch-user-${Ke} must appear in the single batched payload`),e(Array.isArray(Fe?.groups)&&(Fe?.groups).includes("batch-group"),`subject batch-user-${Ke} must carry the granted group membership`)}let We=Ge.find((Ke)=>Ke.unique_id==="batch-user-0");e(We?.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 Le().oaa.clear(),await $s(Le(),Ne,async(ze)=>{await ze.authz.oaa.pushAccessChanges([])}),t((await Le().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 Le().oaa.clear();let ze=await gt(Le().baseUrl,`${Gt.id} ${Qt.id}`),De=await wt(),Ve=new it,Je=new lt,Ge=ot(en(Le(),"ecv-scim-oaa-idempotent"),{validators:rt(),stores:{catalogStore:De.catalog,accessStore:Ve,accessIndex:Je}});try{await Ge.ready(Ne),e(Ge.authz.oaa.isReady(),"OAA must be ready so the write path projects each grant");let We={Authorization:`Bearer ${ze}`,...Nt},Ke=()=>new Request("https://ecv.test/api/ionite/iam/Groups/staff",{method:"PATCH",headers:We,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:"idem-user"}]}]})}),Fe=async()=>{let ct=new Set;for(let ft of await Le().oaa.pushes())for(let ut of ht(Vt(ft.payload)?.local_users)){if(ut.operation==="delete")continue;let Rt=typeof ut.unique_id==="string"?ut.unique_id:"";for(let jt of Array.isArray(ut.groups)?ut.groups:[])if(typeof jt==="string")ct.add(`${Rt}:member_of:group:${jt}`)}return ct},He=await Ge.handler(Ke());e(He.status<300,`first membership grant should succeed, got ${He.status}`);let Xe=(await Ve.listByTarget("group","staff")).items.length,Ye=await Fe(),Ze=(await Le().oaa.pushes()).length;e(Xe===1,"the first grant persists exactly one membership root"),e(Ye.has("idem-user:member_of:group:staff"),"the first grant projects the membership edge"),e((await Je.hasPermission("idem-user","read",void 0,"user")).allow,"the granted staff membership confers the editor role closure (read)");let tt=await Ge.handler(Ke());e(tt.status<300,`re-granting the same membership should still succeed, got ${tt.status}`);let Qe=(await Ve.listByTarget("group","staff")).items.length,st=await Fe(),et=(await Le().oaa.pushes()).length;e(Qe===Xe,`re-pushing the same grant must not add IAM entitlement roots (was ${Xe}, now ${Qe})`),e(st.size===Ye.size,`re-pushing the same grant must not add OAA edges (was ${Ye.size}, now ${st.size})`);for(let ct of Ye)e(st.has(ct),`OAA edge ${ct} must remain stable across an identical re-push`);e(et>Ze,"the SDK re-projects on the second grant (the receiver, not the SDK, dedupes the edge)");let nt=await Ge.handler(new Request("https://ecv.test/api/ionite/iam/Groups/staff",{headers:{Authorization:`Bearer ${ze}`}}));e(nt.status===200,`Groups/staff read should succeed, got ${nt.status}`);let pt=((await nt.json()).members??[]).filter((ct)=>ct.value==="idem-user");e(pt.length===1,`the re-granted member must appear exactly once in the SCIM projection, saw ${pt.length}`)}finally{Ge.close?.()}}}]}function tn(){let Be=new Mo;return Be.set({id:"team:acme",type:"team",name:"Acme",description:"Acme team"}),Be}function Ss(Be,Ne){return mt({id:Ne,workload:{jwksUri:`${Be}/workload/certs`,audience:`${Be}/workload`,claimsAsEntitlements:{claims:["scope"]}},iam:{}})}async function gt(Be,Ne){let Le=new URLSearchParams({grant_type:"client_credentials",client_id:"local-workload-client",client_secret:"local-workload-secret",scope:Ne}),ze=await fetch(`${Be}/workload/token`,{method:"POST",headers:{...zd,...Dd},body:Le}),De=await Bd(ze);return e(typeof De.access_token==="string","mock workload token endpoint must return an access_token"),De.access_token}function Aa(Be,Ne){return ot(mt({id:"ecv-workload-scope",workload:{jwksUri:`${Be}/workload/certs`,audience:`${Be}/workload`,...Ne?.claims?{claimsAsEntitlements:{claims:["scope"]}}:{}},...Ne?.authz?{authz:Ne.authz}:{}}),{validators:rt(),...Ne?.catalog?{authz:{catalog:Ne.catalog}}:{},...Ne?.catalog?{stores:{catalogStore:new St}}:{}})}function Br(Be,Ne,Le){return ot(mt({id:"ecv-workload-incoming",workload:{incoming:Ne},...Le?{authz:Le}:{}}),{validators:rt()})}function Gs(Be){return new Request("https://example.test/api/task",{headers:{Authorization:`Bearer ${Be}`}})}async function Oo(Be,Ne,Le){try{await Be()}catch(ze){let De=ze instanceof Error?ze.message:String(ze);if(!De.includes(Ne))throw Error(`${Le}: expected error containing "${Ne}", got "${De}"`);return}throw Error(`${Le}: expected rejection, but the call resolved`)}function Z(Be){let Ne=Be.timeout??5000,Le=()=>Hr(Be.ecv);return[{name:"hasScope* authorize a workload from its token scope claim (allow / deny + 403 mapping)",fn:async()=>{let ze=await gt(Le().baseUrl,"oaa:sync reports:read"),De=Aa(Le().baseUrl);try{await De.ready(Ne);let Ve=Gs(ze),Je=await $o(Ve,De,"oaa:sync","workload");e(Je.allow,"token scope oaa:sync satisfies hasScope"),e(Je.subjectType==="workload","the matched principal is the workload"),e(Je.subject?.id==="local-workload-client","workload id is the client_id");let Ge=await $o(Ve,De,"admin:all","workload");e(!Ge.allow&&Ge.reason==="forbidden","a missing scope is forbidden"),e(Ge.response().status===403,"forbidden scope -> 403"),e((await Jr(Ve,De,["nope","reports:read"],"workload")).allow,"hasAnyScope matches one"),e(!(await qo(Ve,De,["oaa:sync","nope"],"workload")).allow,"hasAllScope needs all"),e((await qo(Ve,De,["oaa:sync","reports:read"],"workload")).allow,"hasAllScope all present")}finally{De.close?.()}}},{name:"claimsAsEntitlements gates whether has* honors a workload token scope (app-wide only)",fn:async()=>{let ze=await gt(Le().baseUrl,Qt.id),De=Gs(ze),Ve=Aa(Le().baseUrl);try{await Ve.ready(Ne);let Ge=await gs(De,Ve,Qt.id,void 0,"workload");e(!Ge.allow,"a token scope does not satisfy has* when the IdP has no claims block")}finally{Ve.close?.()}let Je=Aa(Le().baseUrl,{claims:!0});try{await Je.ready(Ne);let Ge=await gs(De,Je,Qt.id,void 0,"workload");e(Ge.allow,"app-wide has* honors the matching token scope when the IdP opts in"),e(Ge.decision?.metadata?.reason==="claims","allow reason is claims");let We=await gs(De,Je,Qt.id,"project-a","workload");e(!We.allow,"a direct claim match cannot satisfy a resource-scoped has* check")}finally{Je.close?.()}}},{name:"claimsAsEntitlements resolves an App permission id through the code catalog",fn:async()=>{let De=ma({permissions:[{id:"app:reports:read",name:"Read reports",description:"Read App reports",assignableSubjectTypes:["workload"]}]}),Ve=await gt(Le().baseUrl,"app:reports:read"),Je=Aa(Le().baseUrl,{catalog:De,claims:!0});try{await Je.ready(Ne);let Ge=await gs(Gs(Ve),Je,"app:reports:read",void 0,"workload");e(Ge.allow,"the App permission id resolves through authz.catalog"),e(Ge.decision?.metadata?.reason==="claims","allow reason is claims")}finally{Je.close?.()}}},{name:"trustedProvisioningClients overlay authorizes a matching workload without a store grant or scope",fn:async()=>{let ze=Le().baseUrl,De=`${ze}/workload`,Ve=await gt(ze,""),Je={issuer:De,audience:[De],clientId:"local-workload-client",permissions:[sn.id]},Ge=Aa(ze,{authz:{trustedProvisioningClients:[Je]}});try{await Ge.ready(Ne);let We=Gs(Ve),Ke=await gs(We,Ge,sn.id,void 0,"workload");e(Ke.allow,"trusted client is authorized for its declared permission via the overlay"),e(Ke.decision?.metadata?.reason==="trusted-client","allow reason is trusted-client");let Fe=await gs(We,Ge,"ion:iam:read",void 0,"workload");e(!Fe.allow,"a permission outside the trusted client declaration is denied")}finally{Ge.close?.()}}},{name:"trustedProvisioningClients overlay is audience-bound (a mismatched audience does not authorize)",fn:async()=>{let ze=Le().baseUrl,De=`${ze}/workload`,Ve=await gt(ze,""),Je={issuer:De,audience:["https://some-other-app.example/api"],clientId:"local-workload-client",permissions:[sn.id]},Ge=Aa(ze,{authz:{trustedProvisioningClients:[Je]}});try{await Ge.ready(Ne);let We=await gs(Gs(Ve),Ge,sn.id,void 0,"workload");e(!We.allow,"audience mismatch means the trusted-client overlay does not apply")}finally{Ge.close?.()}}},{name:"per-IdP claimsAsEntitlements honors token scope for that issuer only",fn:async()=>{let ze=Le().baseUrl,De=`${ze}/workload`,Ve=await gt(ze,Qt.id),Je=Br(ze,[{issuer:De,jwksUri:`${ze}/workload/certs`,audience:De,claimsAsEntitlements:{claims:["scope"]}}]);try{await Je.ready(Ne);let Ge=await gs(Gs(Ve),Je,Qt.id,void 0,"workload");e(Ge.allow,"the matched IdP claims block honors the token scope"),e(Ge.decision?.metadata?.reason==="claims","allow reason is claims")}finally{Je.close?.()}}},{name:"multi-IdP incoming enforces audience (cross-app token reuse is rejected at validation)",fn:async()=>{let ze=Le().baseUrl,De=`${ze}/workload`,Ve=await gt(ze,"run_oaa_sync"),Je=Br(ze,[{issuer:De,jwksUri:`${ze}/workload/certs`,audience:"https://other-app.example/api"}]);try{await Je.ready(Ne);let Ge=await gs(Gs(Ve),Je,"run_oaa_sync",void 0,"workload");e(!Ge.allow,"a token whose audience does not match the trusted IdP is rejected"),e(Ge.reason==="unauthenticated","audience mismatch fails validation -> unauthenticated")}finally{Je.close?.()}}},{name:"trustedProvisioningClients without an audience is rejected at config apply (§1.4)",fn:async()=>{let ze=Le().baseUrl,De={issuer:`${ze}/workload`,audience:[],clientId:"eci-provisioning",permissions:["ion:iam:read"]},Ve=ot(mt({id:"ecv-1-4-empty-audience",workload:{jwksUri:`${ze}/workload/certs`,audience:`${ze}/workload`},authz:{trustedProvisioningClients:[De]}}),{validators:rt()});try{await Oo(()=>Ve.ready(Ne),"audience","an audience-less trusted provisioning client must fail config apply")}finally{Ve.close?.()}}},{name:"incoming workload trust without an audience is rejected at config apply (§1.5)",fn:async()=>{let ze=Le().baseUrl,De=ot(mt({id:"ecv-1-5-flat-no-audience",workload:{jwksUri:`${ze}/workload/certs`}}),{validators:rt()});try{await Oo(()=>De.ready(Ne),"audience","a server-only workload trust without an audience must fail config apply")}finally{De.close?.()}let Ve=ot(mt({id:"ecv-1-5-incoming-no-audience",workload:{incoming:[{issuer:`${ze}/workload`,jwksUri:`${ze}/workload/certs`}]}}),{validators:rt()});try{await Oo(()=>Ve.ready(Ne),"audience","an audience-less TrustedIdp incoming entry must fail config apply")}finally{Ve.close?.()}}},{name:"flat server-only workload trust enforces its configured audience at runtime (§1.5)",fn:async()=>{let ze=Le().baseUrl,De=await gt(ze,"run_oaa_sync"),Ve=ot(mt({id:"ecv-1-5-flat-wrong-audience",workload:{jwksUri:`${ze}/workload/certs`,audience:"https://other-app.example/api",claimsAsEntitlements:{claims:["scope"]}}}),{validators:rt()});try{await Ve.ready(Ne);let Je=await gs(Gs(De),Ve,"run_oaa_sync",void 0,"workload");e(!Je.allow,"a token minted for another audience must not authenticate"),e(Je.reason==="unauthenticated","flat audience mismatch -> unauthenticated")}finally{Ve.close?.()}}},{name:"subject-disabled marker vetoes the scope overlay; re-enable restores access",fn:async()=>{let ze=Le().baseUrl,De=await gt(ze,Qt.id),Ve=new lt,Je=ot(mt({id:"ecv-subject-disabled-overlay",workload:{jwksUri:`${ze}/workload/certs`,audience:`${ze}/workload`,claimsAsEntitlements:{claims:["scope"]}}}),{validators:rt(),stores:{accessIndex:Ve}});try{await Je.ready(Ne);let Ge=Gs(De),We=await gs(Ge,Je,Qt.id,void 0,"workload");e(We.allow,"the claims overlay allows before the subject is disabled"),await Ve.setSubjectDisabled("local-workload-client","workload",!0);let Ke=await gs(Ge,Je,Qt.id,void 0,"workload");e(!Ke.allow,"a disabled subject is denied even when a claims overlay matches (deny wins)"),await Ve.setSubjectDisabled("local-workload-client","workload",!1);let Fe=await gs(Ge,Je,Qt.id,void 0,"workload");e(Fe.allow,"re-enabling the subject restores overlay-based access")}finally{Je.close?.()}}},{name:"SCIM active flip sets/clears the AccessIndex subject-disabled marker (deactivate, re-activate, delete)",fn:async()=>{let ze=Le().baseUrl,De=await gt(ze,`${Wr.id} ${Qt.id}`),Ve=new lt,Je=ot(Ss(ze,"ecv-scim-subject-disabled"),{validators:rt(),stores:{accessIndex:Ve,userStore:new Gr}});try{await Je.ready(Ne);let Ge={Authorization:`Bearer ${De}`,...Nt},We=await Je.handler(new Request("https://ecv.test/api/ionite/iam/Users",{method:"POST",headers:Ge,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],userName:"disable-flow@example.test",active:!0})}));e(We.status===201,`SCIM user create should succeed, got ${We.status}`);let Ke=await We.json();e(!await Ve.isSubjectDisabled(Ke.id,"user"),"an active user starts enabled");let Fe=await Je.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${Ke.id}`,{method:"PATCH",headers:Ge,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"active",value:!1}]})}));e(Fe.status===200,`SCIM deactivate should succeed, got ${Fe.status}`),e(await Ve.isSubjectDisabled(Ke.id,"user"),"PATCH active:false sets the disabled marker");let He=await Ve.hasPermission(Ke.id,"read",void 0,"user");e(!He.allow,"a disabled subject is denied by the index"),e(He.metadata?.reason==="subject_disabled","the index denial carries reason subject_disabled");let Xe=await Je.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${Ke.id}`,{method:"PUT",headers:Ge,body:JSON.stringify({schemas:["urn:ietf:params:scim:schemas:core:2.0:User"],id:Ke.id,userName:"disable-flow@example.test",active:!0})}));e(Xe.status===200,`SCIM reactivate should succeed, got ${Xe.status}`),e(!await Ve.isSubjectDisabled(Ke.id,"user"),"PUT active:true clears the disabled marker"),await Je.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${Ke.id}`,{method:"PATCH",headers:Ge,body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"active",value:!1}]})})),e(await Ve.isSubjectDisabled(Ke.id,"user"),"the marker is set again before delete");let Ye=await Je.handler(new Request(`https://ecv.test/api/ionite/iam/Users/${Ke.id}`,{method:"DELETE",headers:Ge}));e(Ye.status===204,`SCIM user delete should succeed, got ${Ye.status}`),e(!await Ve.isSubjectDisabled(Ke.id,"user"),"user delete clears the disabled marker")}finally{Je.close?.()}}}]}async function de(Be={}){let Ne=performance.now(),Le=[...u().tests];if(Be.ecv)Le.push(...Z(Be)),Le.push(...p(Be));let ze=[];for(let Ge of Le)ze.push(await s(Ge.name,async()=>{await Ge.fn();return}));let De=performance.now()-Ne,Ve=ze.filter((Ge)=>!Ge.passed).length,Je=ze.filter((Ge)=>Ge.details?.skipped).length;return{suite:"Authz",passed:Ve===0,tests:ze,duration:De,summary:{total:ze.length,passed:ze.length-Ve-Je,failed:Ve,skipped:Je}}}import{ACCEPT_JSON as Vp,ION_CAPABILITY_CATALOG as Bp,ION_CAPABILITY_CATALOG_VERSION as Jp,readJsonResponse as Gp}from"@ionite/server";import{CONTENT_TYPE_JSON as Jd,readJsonResponse as Lo}from"@ionite/core";import{runLoginToCallback as Gd}from"@ionite/devtools";function Wd(Be){return(Be.headers.getSetCookie?.()??[]).map((Ne)=>Ne.split(";")[0]).join("; ")}async function Kd(Be,Ne,Le){let ze=await Be(Ne.customerProvidersPath);e(ze.status===200,`CIAM provider discovery should succeed, got ${ze.status}`);let Ve=(await Lo(ze)).providers?.find((Je)=>Je.name&&Je.type!=="magic-link"&&(Le===void 0||Je.name===Le));return e(Ve?.name!==void 0,"customer access-request validation requires a matching OIDC provider"),{loginPath:`${Ne.customerLoginPath}?provider=${encodeURIComponent(Ve.name)}`,provider:Ve.name}}function Yr(Be,Ne,Le,ze,De){let Ve=ze==="user"?"workforce principal":"customer";return{name:`an IdP access request for a ${Ve} progresses through pending and denied or approved claim-backed access`,fn:async()=>{let Je=a(Be),Ge={Authorization:`Bearer ${Ne().env.ION_ECV_CONTROL_TOKEN}`},We=ze==="customer"?await Kd(Je,Le,De.provider):void 0,Ke=We?.loginPath??Le.ssoLoginPath,Fe=async()=>{let tt=await Gd({loginPath:Ke},Je);return e(tt.callback.status===302,`IdP login should complete, got ${tt.callback.status}`),(await Je(De.permissionProbePath,{headers:{...Ge,cookie:Wd(tt.callback)}})).status},He=async()=>{let tt=await Je(De.accessRequestsPath,{method:"POST",headers:{...Ge,...Jd},body:JSON.stringify({subjectType:ze,subject:De.subject,target:De.target,operation:"grant",...We?{provider:We.provider}:{}})});return e(tt.status===200,`access request submission should return 200, got ${tt.status}`),await Lo(tt)},Xe=async(tt)=>{let Qe=`${De.accessRequestsPath}/${encodeURIComponent(tt)}`,st=We?`?provider=${encodeURIComponent(We.provider)}`:"",et=await Je(`${Qe}${st}`,{headers:Ge});return e(et.status===200,`access request read should return 200, got ${et.status}`),await Lo(et)};e(await Fe()===403,`the ${Ve} should start without the requested permission`);let Ye=await He();e(Ye.status==="pending","a new access request should be pending"),e((await Xe(Ye.id)).status==="pending","GET should report the pending request"),await Ne().decideAccessRequest(Ye.id,"denied"),e((await Xe(Ye.id)).status==="denied","GET should report the denied request"),e(await Fe()===403,"a denied request must not add the entitlement claim");let Ze=await He();e(Ze.id!==Ye.id,"a denied request should not be reused"),await Ne().decideAccessRequest(Ze.id,"approved"),e((await Xe(Ze.id)).status==="approved","GET should report the approved request"),e(await Fe()===200,`a fresh ${ze==="user"?"SSO":"CIAM"} login should receive the approved entitlement claim`)}}}function zo(Be,Ne,Le,ze){let De=[];if(ze.users)De.push(Yr(Be,Ne,Le,"user",ze.users));if(ze.customers)De.push(Yr(Be,Ne,Le,"customer",ze.customers));return De}import{ACCEPT_JSON as Ks,CONTENT_TYPE_FORM as Xd,CONTENT_TYPE_JSON as nn,readJsonResponse as Jo,silentLogger as Wo}from"@ionite/core";import{ciam as Ko,clearDiscoveryCache as Zd,clearJwksCache as Qd}from"@ionite/core/server";import{InMemoryCustomerStore as Go,InMemoryMagicLinkStore as el,InMemorySessionStore as Ln,ionite as tl}from"@ionite/server";import{createValidators as zn}from"@ionite/valibot";import*as ga from"node:crypto";import{CONTENT_TYPE_JSON as Zr}from"@ionite/core";import{clearJwksCache as Nn,MICROSOFT_CONSUMERS_TENANT_ID as Fd,setJwksClock as Bo,setJwksRetry as Xr,verifyIdToken as _t,verifyLogoutToken as $n}from"@ionite/core/server";import{createValidators as Hd}from"@ionite/valibot";var Ut="https://idp.ecv.local/realms/ecv",yt="ecv-oidc-client",Yd="http://schemas.openid.net/event/backchannel-logout",Ia="https://login.microsoftonline.com/common/v2.0",ws="11111111-2222-3333-4444-555555555555",Pa="99999999-8888-7777-6666-555555555555";function an(Be){return`https://login.microsoftonline.com/${Be}/v2.0`}function ha(Be,Ne={}){let Le=Math.floor(Date.now()/1000);return{iss:an(Be),tid:Be,aud:yt,sub:"ms-user",iat:Le,exp:Le+3600,...Ne}}function bt(Be){let{publicKey:Ne,privateKey:Le}=ga.generateKeyPairSync("rsa",{modulusLength:2048}),ze=Ne.export({format:"jwk"});return{privateKey:Le,kid:Be,jwk:{kty:ze.kty,n:ze.n,e:ze.e,kid:Be,use:"sig",alg:"RS256"}}}function Do(Be){return Buffer.from(Be).toString("base64url")}function vt(Be){let Ne={alg:Be.headerAlg??"RS256",typ:"JWT"};if(Be.headerKid!==null)Ne.kid=Be.headerKid??Be.key.kid;let Le=`${Do(JSON.stringify(Ne))}.${Do(JSON.stringify(Be.claims))}`,ze=ga.createSign("RSA-SHA256");return ze.update(Le),`${Le}.${Do(ze.sign(Be.key.privateKey))}`}function Wt(Be={}){let Ne=Math.floor(Date.now()/1000);return{iss:Ut,aud:yt,sub:"ecv-user",iat:Ne,exp:Ne+3600,...Be}}async function Lt(Be,Ne,Le){let ze=globalThis.fetch;Nn(Be),globalThis.fetch=async(De,Ve)=>{if((typeof De==="string"?De:De.toString())===Be)return new Response(JSON.stringify(Ne),{status:200,headers:Zr});return ze(De,Ve)};try{return await Le()}finally{globalThis.fetch=ze,Nn(Be)}}async function ns(Be,Ne,Le){try{await Be()}catch(ze){let De=ze instanceof Error?ze.message:String(ze);if(!De.includes(Ne))throw Error(`${Le}: expected error containing "${Ne}", got "${De}"`);return}throw Error(`${Le}: expected rejection, but the call resolved`)}function xt(){return`https://jwks.ecv.local/${ga.randomUUID()}`}function Vo(Be={}){let Ne=Math.floor(Date.now()/1000);return{iss:Ut,aud:yt,iat:Ne,jti:ga.randomUUID(),sid:"ecv-session-1",events:{[Yd]:{}},...Be}}async function Mn(Be,Ne,Le){let ze=globalThis.fetch;Nn(Be),Xr({maxRetries:0}),globalThis.fetch=async(De,Ve)=>{if((typeof De==="string"?De:De.toString())===Be){let Ge=Ne();if(Ge==="fail")return new Response("unavailable",{status:503});return new Response(JSON.stringify(Ge),{status:200,headers:Zr})}return ze(De,Ve)};try{return await Le()}finally{globalThis.fetch=ze,Nn(Be),Bo(),Xr()}}function m(){return{tests:[{name:"valid ID token (correct iss/aud/exp/kid) resolves claims",fn:async()=>{let Ne=bt("ecv-valid-kid"),Le=xt(),ze=vt({key:Ne,claims:Wt()}),De=await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}));if(De.sub!=="ecv-user")throw Error(`expected sub "ecv-user", got "${String(De.sub)}"`)}},{name:"wrong aud and wrong azp throws audience mismatch",fn:async()=>{let Ne=bt("ecv-aud-kid"),Le=xt(),ze=vt({key:Ne,claims:Wt({aud:"someone-else",azp:"someone-else"})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"audience mismatch","wrong aud/azp"))}},{name:"matching azp only (client absent from aud) passes",fn:async()=>{let Ne=bt("ecv-azp-kid"),Le=xt(),ze=vt({key:Ne,claims:Wt({aud:"api://resource-server",azp:yt})}),De=await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}));if(De.azp!==yt)throw Error(`expected azp "${yt}", got "${String(De.azp)}"`)}},{name:"wrong iss throws issuer mismatch",fn:async()=>{let Ne=bt("ecv-iss-kid"),Le=xt(),ze=vt({key:Ne,claims:Wt({iss:"https://attacker.example/realms/evil"})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"issuer mismatch","wrong iss"))}},{name:"expired exp throws (beyond clock skew)",fn:async()=>{let Ne=bt("ecv-exp-kid"),Le=xt(),ze=Math.floor(Date.now()/1000)-3600,De=vt({key:Ne,claims:Wt({exp:ze})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:De,jwksUri:Le,issuer:Ut,audience:yt}),"expired","expired token"))}},{name:"missing kid in header throws",fn:async()=>{let Ne=bt("ecv-nokid-kid"),Le=xt(),ze=vt({key:Ne,headerKid:null,claims:Wt()});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"missing kid","missing kid"))}},{name:"unknown header kid fails closed after JWKS refresh (no first-key fallback)",fn:async()=>{let Ne=bt("ecv-realkid"),Le=xt(),ze=vt({key:Ne,headerKid:"kid-not-in-jwks",claims:Wt()});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"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 Ne=bt("kid-1"),Le=bt("kid-2"),ze=xt(),De=[Ne.jwk];await Mn(ze,()=>({keys:De}),async()=>{if(await _t({idToken:vt({key:Ne,claims:Wt()}),jwksUri:ze,issuer:Ut,audience:yt}),De=[Ne.jwk,Le.jwk],(await _t({idToken:vt({key:Le,claims:Wt()}),jwksUri:ze,issuer:Ut,audience:yt})).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 Ne=bt("kid-cold"),Le=xt(),ze=vt({key:Ne,claims:Wt()});await Mn(Le,()=>"fail",()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"Failed to fetch JWKS","cold JWKS 503"))}},{name:"JWKS TTL expiry triggers a refresh that picks up rotation (§1.10)",fn:async()=>{let Ne=bt("kid-ttl-1"),Le=bt("kid-ttl-2"),ze=xt(),De=Date.UTC(2026,0,1);Bo(()=>De);let Ve=[Ne.jwk];await Mn(ze,()=>({keys:Ve}),async()=>{if(await _t({idToken:vt({key:Ne,claims:Wt()}),jwksUri:ze,issuer:Ut,audience:yt}),Ve=[Le.jwk],De+=660000,(await _t({idToken:vt({key:Le,claims:Wt()}),jwksUri:ze,issuer:Ut,audience:yt})).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 Ne=bt("kid-warm"),Le=xt(),ze=Date.UTC(2026,0,1);Bo(()=>ze);let De="ok";await Mn(Le,()=>De==="fail"?"fail":{keys:[Ne.jwk]},async()=>{if(await _t({idToken:vt({key:Ne,claims:Wt()}),jwksUri:Le,issuer:Ut,audience:yt}),ze+=660000,De="fail",(await _t({idToken:vt({key:Ne,claims:Wt()}),jwksUri:Le,issuer:Ut,audience:yt})).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 Ne=bt("kid-logout-ok"),Le=xt(),ze=vt({key:Ne,claims:Vo()}),De=await Lt(Le,{keys:[Ne.jwk]},()=>$n({logoutToken:ze,jwksUri:Le,issuer:Ut,audience:yt}));if(De.sid!=="ecv-session-1")throw Error(`expected sid, got "${String(De.sid)}"`)}},{name:"logout_token containing a nonce is rejected (§1.6)",fn:async()=>{let Ne=bt("kid-logout-nonce"),Le=xt(),ze=vt({key:Ne,claims:Vo({nonce:"must-not-be-here"})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>$n({logoutToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"must not contain a nonce","logout_token nonce"))}},{name:"plain ID token (no events) rejected as a logout_token (§1.6)",fn:async()=>{let Ne=bt("kid-logout-idtoken"),Le=xt(),ze=vt({key:Ne,claims:Wt({sid:"ecv-session-1"})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>$n({logoutToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"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 Ne=bt("kid-logout-nosub"),Le=xt(),ze=vt({key:Ne,claims:Vo({sid:void 0,sub:void 0})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>$n({logoutToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"missing sid and sub","logout_token no sid/sub"))}},{name:"nonce: matching expectedNonce at the callback exchange passes (§1.8)",fn:async()=>{let Ne=bt("ecv-nonce-ok"),Le=xt(),ze=vt({key:Ne,claims:Wt({nonce:"ecv-nonce-123"})});if((await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt,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 Ne=bt("ecv-nonce-bad"),Le=xt(),ze=vt({key:Ne,claims:Wt({nonce:"attacker-nonce"})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt,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 Ne=bt("ecv-nonce-missing"),Le=xt(),ze=vt({key:Ne,claims:Wt()});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt,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 Ne=bt("ecv-nonce-refresh"),Le=xt(),ze=vt({key:Ne,claims:Wt()});if((await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}))).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 Ne=bt("ecv-alg-none"),Le=xt(),ze=vt({key:Ne,headerAlg:"none",claims:Wt()});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"Unsupported JWT alg","alg none"))}},{name:"alg pinning: HMAC header alg (HS256) is rejected (§1.9)",fn:async()=>{let Ne=bt("ecv-alg-hs"),Le=xt(),ze=vt({key:Ne,headerAlg:"HS256",claims:Wt()});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"Unsupported JWT alg","alg HS256"))}},{name:"alg pinning: an RSA JWK cannot verify a token claiming ES256 (§1.9)",fn:async()=>{let Ne=bt("ecv-alg-cross"),Le=xt(),ze=vt({key:Ne,headerAlg:"ES256",claims:Wt()});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ut,audience:yt}),"does not match token alg","alg/JWK-type cross"))}},{name:"microsoft-tenant policy accepts a tenant-scoped iss under a /common authority",fn:async()=>{let Ne=bt("ms-any-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(ws)}),De=await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:Ia,issuerPolicy:{mode:"microsoft-tenant",allowedTenants:"any"},audience:yt}));if(De.tid!==ws)throw Error(`expected tid "${ws}", got "${String(De.tid)}"`)}},{name:"microsoft-tenant policy accepts a personal (MSA/consumers) account",fn:async()=>{let Ne=bt("ms-msa-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(Fd)});await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:Ia,issuerPolicy:{mode:"microsoft-tenant",allowedTenants:"consumers"},audience:yt}))}},{name:"microsoft-tenant allowlist rejects a tenant that is not listed",fn:async()=>{let Ne=bt("ms-allowlist-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(Pa)});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ia,issuerPolicy:{mode:"microsoft-tenant",allowedTenants:[ws]},audience:yt}),"issuer mismatch","unlisted tenant"))}},{name:"microsoft-tenant policy rejects a look-alike issuer host",fn:async()=>{let Ne=bt("ms-host-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(ws,{iss:`https://login.microsoftonline.com.evil.test/${ws}/v2.0`})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ia,issuerPolicy:{mode:"microsoft-tenant",allowedTenants:"any"},audience:yt}),"issuer mismatch","look-alike host"))}},{name:"microsoft-tenant policy rejects a tid that disagrees with iss",fn:async()=>{let Ne=bt("ms-tid-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(ws,{tid:Pa})});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ia,issuerPolicy:{mode:"microsoft-tenant",allowedTenants:"any"},audience:yt}),"issuer mismatch","tid/iss disagreement"))}},{name:"default (exact) policy still rejects a tenant-scoped iss under /common",fn:async()=>{let Ne=bt("ms-exact-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(ws)});await Lt(Le,{keys:[Ne.jwk]},()=>ns(()=>_t({idToken:ze,jwksUri:Le,issuer:Ia,audience:yt}),"issuer mismatch","exact policy under /common"))}},{name:"single-tenant microsoft app keeps strict equality (exact authority still verifies)",fn:async()=>{let Ne=bt("ms-single-kid"),Le=xt(),ze=vt({key:Ne,claims:ha(ws)});await Lt(Le,{keys:[Ne.jwk]},()=>_t({idToken:ze,jwksUri:Le,issuer:an(ws),audience:yt}))}},{name:"token response validator rejects a body missing access_token (§1.11 refresh fails closed)",fn:async()=>{let Ne=Hd();if(!(await Ne.sso.tokenResponse.validate({token_type:"Bearer"})).issues)throw Error("expected validation issues for a token response missing access_token");if((await Ne.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 le(){let Be=performance.now(),{tests:Ne}=m(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"SSO ID token verification",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}var Qr={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"},sl=["clientId","clientSecret","authority","privateKey","keyId","teamId"];function al(Be){return Be.endsWith(".access")||Be.endsWith(".id")||Be.endsWith(".control")}function nl(Be){for(let Ne of Be.keys()){if(Ne.endsWith(".session"))return Ne.slice(0,-8);let Le=Ne.indexOf(".cstate");if(Le>0)return Ne.slice(0,Le)}return}async function ei(Be,Ne){return s("CIAM Customer Endpoint (Unauthenticated)",async()=>{let Le=Be.customerPath??"/api/ionite/auth/customer",ze=await Ne(Le,{headers:Ks}),De=ze.status!==401?await ze.clone().text():"";return e(ze.status===401,`Expected 401 Unauthorized, got ${ze.status} (content-type=${ze.headers.get("content-type")}, body=${De.slice(0,200)})`),{details:{status:ze.status}}})}async function ti(Be,Ne){return s("CIAM Magic Link Requires Workload Auth",async()=>{let Le=Be.magicLinkPath??"/api/ionite/magic-link",ze=await Ne(Le,{method:"POST",headers:{...Ks,...nn},body:JSON.stringify({userName:"jdoe",name:"Jane Doe",email:"jane@example.com"})});return e(ze.status===401||ze.status===404,`Expected 401 Unauthorized (or 404 if disabled), got ${ze.status}`),{details:{status:ze.status,endpointExists:ze.status!==404}}})}async function si(Be,Ne){e(Ne.endsWith("/logout"),`CIAM logoutPath must end with /logout, got: ${Ne}`);let Le={headers:Ks},ze=await Be(Ne,Le);if(ze.status!==200&&ze.status!==302)ze=await Be(Ne,Le);if(ze.status!==200&&ze.status!==302){let De=await ze.text().catch(()=>"");throw Error(`Expected 302 or 200, got ${ze.status} for ${Ne} (content-type=${ze.headers.get("content-type")??"none"}): ${De.slice(0,500)}`)}return ze}async function ai(Be,Ne){return s("CIAM Customer Logout Endpoint",async()=>{let Le=Be.logoutPath??"/api/ionite/auth/customer/logout",ze=await si(Ne,Le),De=ze.headers.getSetCookie?.()??[],Ve=De.some((We)=>We.includes("Max-Age=0")||We.includes("expires=Thu, 01 Jan 1970")),Je=De.some((We)=>We.startsWith("ion.sso")&&(We.includes("Max-Age=0")||We.includes("expires=")));e(!Je,"CIAM logout must not clear SSO (ion.sso.*) cookies");let Ge=[...qs(ze.headers).entries()].filter(([,We])=>We===""||We.includes("Max-Age=0")).map(([We])=>We);return e(Ve||Ge.length>0||De.length===0,"CIAM logout should clear customer session cookies"),{details:{status:ze.status,clearedCookies:Ge}}})}async function ni(Be,Ne){return s("CIAM Back-Channel Logout Removed",async()=>{let Le=Be.backChannelLogoutPath??"/api/ionite/auth/customer/logout/backchannel",ze=await Ne(Le,{method:"POST",headers:{...Ks,...Xd},body:""});return e(ze.status===404,`Expected 404 (route removed), got ${ze.status}`),{details:{status:ze.status,endpointRemoved:!0}}})}async function oi(Be,Ne){return s("CIAM Provider Discovery (Sanitized + Ordered)",async()=>{let Le=Be.customerProvidersPath??"/api/ionite/auth/customer/providers",ze=await Ne(Le,{headers:Ks});if(ze.status===404)return{details:{status:404,configured:!1,skipped:!0}};e(ze.status===200,`Expected 200, got ${ze.status}`);let De=await Jo(ze);e(Array.isArray(De.providers),"Discovery response must contain a `providers` array");let Ve=De.providers,Je=[];for(let We of Ve){e(typeof We.name==="string","Each provider must have a string `name`"),e(typeof We.loginUrl==="string","Each provider must include a derived `loginUrl`"),e(typeof We.displayName==="string","Each provider must include a `displayName`");for(let Ke of sl)e(!(Ke in We),`Discovery must not expose secret field "${Ke}"`);Je.push(We.name)}let Ge=await Ne(Le,{headers:Ks});if(Ge.status===200){let Ke=((await Jo(Ge)).providers??[]).map((Fe)=>Fe.name);e(JSON.stringify(Je)===JSON.stringify(Ke),"Provider discovery order must be stable (recommended order preserved)")}return{details:{status:200,configured:!0,count:Ve.length,order:Je}}})}async function ri(Be,Ne){return s("CIAM Third-Party Login Redirect",async()=>{let Le=Be.customerLoginPath??"/api/ionite/auth/customer/login",ze=await Ne(`${Le}?provider=__ecv_unknown__`,{redirect:"manual"});e(ze.status===404||ze.status===400||ze.status===500,`Unknown provider should be rejected (4xx/5xx), got ${ze.status}`);let De=Be.customerProvidersPath??"/api/ionite/auth/customer/providers",Ve=await Ne(De,{headers:Ks});if(Ve.status!==200)return{details:{unknownStatus:ze.status,configured:!1,skipped:!0}};let Ge=((await Jo(Ve)).providers??[]).find((Xe)=>Xe.type&&Xe.type!=="magic-link");if(!Ge?.name)return{details:{unknownStatus:ze.status,configured:!1,skipped:!0}};let We=await Ne(`${Le}?provider=${encodeURIComponent(Ge.name)}`,{redirect:"manual"});e(We.status===302,`Expected 302 redirect to IdP, got ${We.status}`);let Ke=We.headers.get("location");e(Boolean(Ke),"Login redirect must include a Location header"),e((Ke??"").includes("state=")&&(Ke??"").includes("client_id="),"Authorization redirect must include OAuth2 state and client_id parameters");let He=(We.headers.getSetCookie?.()??[]).find((Xe)=>Xe.includes(".cstate_"));return e(Boolean(He),"Login must set a CIAM state cookie (ion.ciam.cstate_<state>)"),e(/SameSite=Lax/i.test(He),`CIAM state cookie must be SameSite=Lax for the cross-site provider callback, got: ${He}`),{details:{status:302,provider:Ge.name,unknownStatus:ze.status,cstateCookie:He}}})}async function ii(Be,Ne){return s("CIAM Logout Opaque-Only (no token cookies)",async()=>{let Le=Be.logoutPath??"/api/ionite/auth/customer/logout",De=(await si(Ne,Le)).headers.getSetCookie?.()??[],Ve=De.find((Je)=>al(Je.split("=")[0]?.trim()??""));return e(!Ve,`CIAM logout must not touch token cookies (opaque-only), found: ${Ve}`),{details:{setCookieCount:De.length}}})}async function ci(Be,Ne){return s("CIAM Legacy Token Cookies Rejected",async()=>{let Le=Be.logoutPath??"/api/ionite/auth/customer/logout",ze=Be.customerPath??"/api/ionite/auth/customer",De=await Ne(Le,{headers:Ks}),Ve=nl(qs(De.headers));if(!Ve)return{details:{skipped:!0,reason:"Could not derive CIAM cookie prefix from logout response"}};let Je=new Map([[`${Ve}.access`,"legacy-access"],[`${Ve}.id`,"legacy-id"],[`${Ve}.control`,btoa(JSON.stringify({token_type:"Bearer",expires_in:3600,expires:new Date(Date.now()+3600000).toISOString()}))]]),Ge=await Ne(ze,{headers:{...Ks,Cookie:oa(Je)}});return e(Ge.status===401,`Legacy CIAM token cookies must not authenticate (expected 401), got ${Ge.status}`),{details:{status:Ge.status,prefix:Ve}}})}async function ue(Be){let Ne=performance.now(),Le={...Qr,...Be},ze=a(Le),De=[];De.push(await ei(Le,ze)),De.push(await ti(Le,ze)),De.push(await ai(Le,ze)),De.push(await ii(Le,ze)),De.push(await ci(Le,ze)),De.push(await ni(Le,ze)),De.push(await oi(Le,ze)),De.push(await ri(Le,ze));let Ve=performance.now()-Ne,Je=De.filter((Ke)=>Ke.passed).length,Ge=De.filter((Ke)=>!Ke.passed).length,We=De.filter((Ke)=>Ke.details?.skipped).length;return{suite:"CIAM",passed:Ge===0,tests:De,duration:Ve,summary:{total:De.length,passed:Je-We,failed:Ge,skipped:We}}}async function ol(){return s("CIAM magic-link session stores no token material (§1.10)",async()=>{let Be=new el,Ne=new Ln,Le=zn(),ze=Ko(Le.ciam,Wo,void 0,{magicLinkUrl:"/magic-link",magicLinkLoginUrl:"/magic-link/login",sessionStore:Ne,magicLinkStore:Be,cookiesSecure:!1});e(!!ze,"ciam() must return a handle when configured");let De={id:"cust-1",userName:"cust1",email:"c1@example.test",name:"Customer One"};await Be.create("tok-1",De,new Date(Date.now()+60000));let Ve=await ze?.handler(new Request("https://app.ecv.local/magic-link/login?token=tok-1"));e(!!Ve,"magic-link login must produce a response");let Ge=(Ve?.headers.getSetCookie?.()??[]).find((Fe)=>(Fe.split("=")[0]?.trim()??"").endsWith(".session"));e(!!Ge,"magic-link login must set an opaque session cookie");let We=Ge.split("=")[1]?.split(";")[0]??"";e(We.length>0,"session cookie must carry an opaque sid");let Ke=await Ne.get(We);return e(Ke!=null,"the magic-link session must be persisted"),e(!("tokens"in Ke),"a CIAM session must NOT store any signed token material (§1.10)"),e(!!Ke?.claims,"the opaque session still carries identity claims"),{details:{sid:We,hasTokens:Ke!=null&&"tokens"in Ke}}})}function rl(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}async function il(){return s("OIDC-only CIAM serves without a magicLinkStore; magic-link create is 501",async()=>{let Be=tl(rl({id:"ciam-oidc-only",ciam:{cookiesSecure:!1,providers:{ioniteDevCIAM:{type:"oidc",enabled:!0,order:1,authority:"https://ciam.example.test",authorizationUrl:"https://ciam.example.test/authorize",tokenUrl:"https://ciam.example.test/token",jwksUri:"https://ciam.example.test/jwks",clientId:"ecv-client",clientSecret:"ecv-secret"}}}}),{validators:zn(),basePath:"/api/ionite",stores:{sessionStore:new Ln,customerStore:new Go}});try{await Be.ready(1000);let Ne=Be.readiness().modules.ciam;e(Ne.active,"Expected CIAM to report active"),e(Ne.ready,`Expected CIAM to be ready without a magicLinkStore, got: ${Ne.reason}`);let Le=await Be.handler(new Request("https://ecv.test/api/ionite/auth/customer/providers"));e(Le.status===200,`Expected providers 200, got ${Le.status}`);let ze=await Le.json();e(ze.providers?.some((Je)=>Je.name==="ioniteDevCIAM")===!0,`Expected ioniteDevCIAM in the discovery list, got: ${JSON.stringify(ze.providers)}`);let De=Ko(zn().ciam,Wo,{},{magicLinkUrl:"/api/ionite/magic-link",sessionStore:new Ln,customerStore:new Go},{getWorkloadIdentity:async()=>({clientId:"ecv",claims:{iss:"ecv",sub:"ecv",exp:0,iat:0}})});e(De!=null,"Expected a CIAM handle without a magicLinkStore");let Ve=await De.handler(new Request("https://ecv.test/api/ionite/magic-link",{method:"POST",headers:nn,body:JSON.stringify({userName:"jdoe",name:"Jane Doe",email:"jane@example.com"})}));e(Ve.status===501,`Expected magic-link create 501 without a store, got ${Ve.status}`)}finally{Be.close?.()}})}async function cl(){return s("microsoft /common authority enforces allowedTenants through the CIAM callback",async()=>{let Ve=new Map([[ws,bt("ms-key-a")],[Pa,bt("ms-key-b")]]),Je=(Ze)=>`https://login.microsoftonline.com/${Ze}/discovery/v2.0/keys`,Ge=(Ze)=>`https://login.microsoftonline.com/${Ze}/v2.0/.well-known/openid-configuration`,We=Pa,Ke="",Fe,He=()=>Fe,Xe=globalThis.fetch,Ye=()=>{Zd();for(let Ze of Ve.keys())Qd(Je(Ze))};Ye(),globalThis.fetch=async(Ze,tt)=>{let Qe=typeof Ze==="string"?Ze:Ze instanceof URL?Ze.toString():Ze.url;if(Qe==="https://login.microsoftonline.com/common/oauth2/v2.0/token"){let st=Ve.get(We);e(st!==void 0,`no test key for tenant ${We}`);let et=Math.floor(Date.now()/1000),nt=vt({key:st,claims:{iss:an(We),tid:We,aud:"ecv-ms-client",sub:`ms-user-${We}`,email:"ms-user@example.com",name:"MS User",nonce:Ke,iat:et,exp:et+3600}});return new Response(JSON.stringify({access_token:"ecv-ms-access",token_type:"Bearer",expires_in:3600,id_token:nt}),{status:200,headers:nn})}for(let[st,et]of Ve){if(Qe===Ge(st))return new Response(JSON.stringify({issuer:an(st),jwks_uri:Je(st)}),{status:200,headers:nn});if(Qe===Je(st))return new Response(JSON.stringify({keys:[et.jwk]}),{status:200,headers:nn})}return Xe(Ze,tt)};try{let Ze=Ko(zn().ciam,Wo,{customerLoginUrl:"/api/ionite/auth/customer/login",customerCallbackUrl:"/api/ionite/auth/customer/callback",providers:{microsoft:{type:"microsoft",clientId:"ecv-ms-client",clientSecret:"ecv-ms-secret",allowedTenants:[ws]}}},{sessionStore:new Ln,customerStore:new Go,enableJitUserProvisioning:!0,afterLogin:(et)=>(Fe=et.ok?{ok:!0}:{ok:!1,error:String(et.error)},new Response(et.ok?"ok":"denied",{status:et.ok?200:401}))});e(Ze!=null,"Expected a CIAM handle for the microsoft provider");let tt=async(et)=>{We=et,Fe=void 0;let nt=await Ze.handler(new Request("https://ecv.test/api/ionite/auth/customer/login?provider=microsoft"));e(nt.status===302,`Expected login redirect 302, got ${nt.status}`);let dt=new URL(nt.headers.get("location")??""),pt=dt.searchParams.get("state")??"";Ke=dt.searchParams.get("nonce")??"",e(pt.length>0&&Ke.length>0,"Authorization URL must carry state and nonce");let ct=(nt.headers.get("set-cookie")??"").split(";")[0];return e(ct.includes(`cstate_${pt}=`),`Expected a cstate cookie for ${pt}, got ${ct}`),Ze.handler(new Request(`https://ecv.test/api/ionite/auth/customer/callback?code=ecv-code&state=${encodeURIComponent(pt)}`,{headers:{cookie:ct}}))},Qe=await tt(Pa);e(Qe.status===401&&He()?.ok===!1,`A token issued by a tenant outside allowedTenants must fail the callback, got ${Qe.status} (${JSON.stringify(He())})`),e((He()?.error??"").includes("issuer"),`Expected the denial to be an issuer-policy failure, got: ${He()?.error}`);let st=await tt(ws);e(st.status===200&&He()?.ok===!0,`A token issued by an allowed tenant must pass the callback, got ${st.status} (${JSON.stringify(He())})`)}finally{globalThis.fetch=Xe,Ye()}})}function h(Be){let Ne={...Qr,...Be},Le=a(Ne),ze={createCustomerTests:()=>[{name:"customer endpoint returns 401 when unauthenticated",fn:async()=>{let Ve=await ei(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"legacy CIAM token cookies do not authenticate (opaque-only)",fn:async()=>{let Ve=await ci(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}],createMagicLinkTests:()=>[{name:"magic-link generation requires workload auth",fn:async()=>{let Ve=await ti(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"magic-link session stores no token material (§1.10)",fn:async()=>{let Ve=await ol();if(!Ve.passed)throw Error(Ve.error)}},{name:"OIDC-only CIAM serves without a magicLinkStore; magic-link create is 501",fn:async()=>{let Ve=await il();if(!Ve.passed)throw Error(Ve.error)}}],createOidcLoginTests:()=>[{name:"microsoft /common authority enforces allowedTenants through the CIAM callback",fn:async()=>{let Ve=await cl();if(!Ve.passed)throw Error(Ve.error)}},{name:"provider discovery returns sanitized list in stable order",fn:async()=>{let Ve=await oi(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"third-party login redirects to IdP with state",fn:async()=>{let Ve=await ri(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}],createLogoutTests:()=>[{name:"customer logout endpoint clears CIAM cookies (not SSO)",fn:async()=>{let Ve=await ai(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"customer logout is opaque-only (no token cookies)",fn:async()=>{let Ve=await ii(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"customer back-channel logout endpoint is removed (404)",fn:async()=>{let Ve=await ni(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}]};return{tests:[...ze.createCustomerTests(),...ze.createMagicLinkTests(),...ze.createOidcLoginTests(),...ze.createLogoutTests()],ext:ze}}import{defineCatalog as kl}from"@ionite/core";import{deriveClaimsEntitlements as Fs}from"@ionite/core/server";import{runLoginToCallback as Dn}from"@ionite/devtools";import{hasPermission as Fo,InMemorySessionStore as di,ION_CAPABILITIEZ_READ as Ho,ionite as Sl}from"@ionite/server";import{createValidators as Tl}from"@ionite/valibot";import{ACCEPT_JSON as dl,CONTENT_TYPE_FORM as ll,readJsonResponse as ul,unexpectedBodySummary as pl}from"@ionite/core";import{ION_CAPABILITIEZ_READ as ml,ION_IAM_ACCESS_REQUESTS_WRITE as hl,ION_IAM_CUSTOMERS_WRITE as gl,ION_IAM_GROUPS_WRITE as fl,ION_IAM_READ as yl,ION_IAM_USERS_WRITE as wl,ION_IAM_WORKLOADS_WRITE as vl,ION_TENANT_MANAGE as bl}from"@ionite/server";var U=`${yl.id} ${wl.id} ${fl.id} ${vl.id} ${gl.id} ${hl.id}`,q=ml.id,l=bl.id;async function o(Be,Ne,Le="local-workload-client",ze="local-workload-secret"){let De=`${Be.replace(/\/$/,"")}/workload/token`,Ve=await fetch(De,{method:"POST",headers:{...dl,...ll},body:new URLSearchParams({grant_type:"client_credentials",client_id:Le,client_secret:ze,...Ne?{scope:Ne}:{}}).toString()});if(!Ve.ok){let Ge=await pl(Ve);throw Error(`Failed to get tenant auth token: ${Ve.status}${Ge.excerpt?` ${Ge.excerpt}`:""}`)}let Je=await ul(Ve);if(!Je.access_token)throw Error("Tenant auth token response missing access_token");return Je.access_token}function Cl(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}var Hs=kl({permissions:[{id:"desk.read",name:"Read a desk",description:"Read one desk",assignableSubjectTypes:["user","customer"],resources:["desk-1"]},{id:"app.admin",name:"Admin",description:"App-wide admin",assignableSubjectTypes:["user","customer"]}],groups:[{id:"admins",name:"Admins",description:"Admin group",assignableSubjectTypes:["user","customer"],permissions:["app.admin"]}]});function Rl(Be,Ne){return{sid:Be,sub:"user-1",createdAt:new Date,expiresAt:new Date(Date.now()+60000),authType:"sso",claims:{sub:"user-1",email:"user@example.test"},tokens:{access_token:"opaque",id_token:"opaque",token_type:"Bearer"},entitlements:Ne}}async function li(Be,Ne,Le){let ze=Sl(Cl({id:"claims-user",sso:{clientId:"claims-client",authority:"https://idp.example.test",jwksUri:"https://idp.example.test/certs",...Ne?{claimsAsEntitlements:{claims:["groups"],expand:!0,resources:!0}}:{}}}),{validators:Tl(),authz:{catalog:Hs},sso:{sessionStore:Be}});try{await ze.ready(1000),await Le(ze)}finally{ze.close?.()}}function pi(){return[{name:"expand resolves a group claim to its permission and drops unknown values",fn:async()=>{let Be=await Fs({block:{claims:["groups"],expand:!0},claims:{groups:["admins","not-a-catalog-id"]},subjectType:"user",subjectId:"user-1",catalog:{code:Hs}});e(Be.some((Ne)=>Ne.target==="app.admin"),"the admin group expands to app.admin"),e(!Be.some((Ne)=>Ne.target==="not-a-catalog-id"),"unknown claim values are dropped")}},{name:"resources stays off unless expand and resources are both set",fn:async()=>{let Be=await Fs({block:{claims:["roles"],expand:!0},claims:{roles:["desk.read"]},subjectType:"user",subjectId:"user-1",catalog:{code:Hs}});e(Be.some((Le)=>Le.target==="desk.read"&&Le.resource===void 0),"expand without resources yields an app-wide row");let Ne=await Fs({block:{claims:["roles"],expand:!0,resources:!0},claims:{roles:["desk.read"]},subjectType:"user",subjectId:"user-1",catalog:{code:Hs}});e(Ne.some((Le)=>Le.target==="desk.read"&&Le.resource==="desk-1"),"resources binds the catalog resource")}},{name:"a later derivation replaces the previous set (refresh re-derives)",fn:async()=>{let Be=await Fs({block:{claims:["groups"],expand:!0},claims:{groups:["admins"]},subjectType:"user",subjectId:"user-1",catalog:{code:Hs}}),Ne=await Fs({block:{claims:["groups"],expand:!0},claims:{groups:[]},subjectType:"user",subjectId:"user-1",catalog:{code:Hs}});e(Be.some((Le)=>Le.target==="app.admin"),"the first derivation includes the group"),e(Ne.length===0,"a refresh with no groups stores an empty set")}},{name:"planted session entitlements satisfy has* and a resource binding requires the resources flag",fn:async()=>{let Be=new di,Ne=await Fs({block:{claims:["groups","roles"],expand:!0,resources:!0},claims:{groups:["admins"],roles:["desk.read"]},subjectType:"user",subjectId:"user-1",catalog:{code:Hs}});await Be.create(Rl("sid-1",Ne)),await li(Be,!0,async(Le)=>{let ze=new Request("https://app.test/admin",{headers:{cookie:"ion.sso.claims-client.session=sid-1"}}),De=await Fo(ze,Le,"app.admin",void 0,"user");e(De.allow,"the expanded group satisfies the app-wide permission"),e(De.decision?.metadata?.reason==="claims","allow reason is claims");let Ve=await Fo(ze,Le,"desk.read","desk-1","user");e(Ve.allow,"a catalog-bound row satisfies the resource-scoped check");let Je=await Fo(ze,Le,"desk.read","desk-2","user");e(!Je.allow,"a different resource does not match")})}},{name:"a session store that does not declare derivedEntitlements is described as incapable",fn:async()=>{let Be=new di;await li({capabilities:{},create:(Le)=>Be.create(Le),get:(Le)=>Be.get(Le),update:(Le,ze)=>Be.update(Le,ze),delete:(Le)=>Be.delete(Le)},!0,async(Le)=>{let ze=Le.sso?.describe();e(ze?.claimsAsEntitlements?.capable===!1,"describe reports the store as incapable"),e(ze?.claimsAsEntitlements?.claims[0]==="groups","describe still reports the configured claims")})}},{name:"CIAM derives per provider and ignores a provider without the block",fn:async()=>{let Be=await Fs({block:{claims:["groups"],expand:!0},claims:{groups:["admins"]},subjectType:"customer",subjectId:"cust-1",catalog:{code:Hs}}),Ne=await Fs({block:{claims:[]},claims:{groups:["admins"]},subjectType:"customer",subjectId:"cust-1",catalog:{code:Hs}});e(Be.some((Le)=>Le.target==="app.admin"),"the configured provider derives the group"),e(Ne.length===0,"an empty claims list derives nothing")}},{name:"SDK ion permission ids match from scope without a catalog row",fn:async()=>{let Be=await Fs({block:{claims:["scope"]},claims:{},scope:Ho.id,subjectType:"workload",subjectId:"local-workload-client"});e(Be.some((Ne)=>Ne.target===Ho.id&&Ne.targetType==="permission"),"an ion permission id resolves from the scope claim")}}]}function Yo(Be,Ne,Le){return[{name:"a workload token whose scope maps to ion:capabilitiez:read can read capabilitiez; an unmapped token is forbidden",fn:async()=>{let ze=a(Be),De=await o(Ne(),Ho.id),Ve=await ze(Le,{headers:{Authorization:`Bearer ${De}`}});e(Ve.status===200,`mapped workload scope should read capabilitiez, got ${Ve.status}`);let Je=await o(Ne(),"unmapped:scope"),Ge=await ze(Le,{headers:{Authorization:`Bearer ${Je}`}});e(Ge.status===403,`an unmapped scope should be forbidden, got ${Ge.status}`)}}]}function Xo(Be,Ne,Le){return[{name:"a workforce login maps the groups claim to an App permission",fn:async()=>{let ze=a(Be),De=await Dn({loginPath:Ne.loginPath},ze,{groups:[Le]});e(De.callback.status===302,`mapped login should complete, got ${De.callback.status}`);let Ve=De.callback.headers.getSetCookie?.()??[],Je=Ve.map((He)=>He.split(";")[0]).join("; ");e(Je.includes(".session="),`mapped login did not return an SSO session cookie: ${Ve.join(" | ")}`);let Ge=await ze(Ne.permissionProbePath,{headers:{cookie:Je}});e(Ge.status===200,`mapped workforce claim should satisfy the App permission, got ${Ge.status}`);let Ke=((await Dn({loginPath:Ne.loginPath},ze,{groups:["not-a-permission"]})).callback.headers.getSetCookie?.()??[]).map((He)=>He.split(";")[0]).join("; "),Fe=await ze(Ne.permissionProbePath,{headers:{cookie:Ke}});e(Fe.status===403,`an unmapped group should be forbidden, got ${Fe.status}`)}}]}function ui(Be){return(Be.headers.getSetCookie?.()??[]).map((Ne)=>Ne.split(";")[0]).join("; ")}function Zo(Be,Ne,Le){return[{name:"a customer login maps the provider groups claim to an App permission",fn:async()=>{let ze=a(Be),De=await ze(Ne.customerProvidersPath);e(De.status===200,`CIAM provider discovery should succeed, got ${De.status}`);let Je=(await De.json()).providers?.find((Xe)=>Xe.name&&Xe.type!=="magic-link");e(Je?.name!==void 0,"CIAM claims authz requires an OIDC provider");let Ge=`${Ne.customerLoginPath}?provider=${encodeURIComponent(Je.name)}`,We=await Dn({loginPath:Ge},ze,{groups:[Le]});e(We.callback.status===302,`mapped customer login should complete, got ${We.callback.status}`);let Ke=await ze(Ne.permissionProbePath,{headers:{cookie:ui(We.callback)}});e(Ke.status===200,`mapped customer claim should satisfy the App permission, got ${Ke.status}`);let Fe=await Dn({loginPath:Ge},ze,{groups:["not-a-catalog-id"]});e(Fe.callback.status===302,`unmapped customer login should complete, got ${Fe.callback.status}`);let He=await ze(Ne.permissionProbePath,{headers:{cookie:ui(Fe.callback)}});e(He.status===403,`an unmapped customer claim should be forbidden, got ${He.status}`)}}]}function Vn(Be,Ne){e(Be.capabilities[Ne]?.testable===!0,Be.capabilities[Ne]?.reason??`${Ne} is not testable`)}import{generateLfvEd25519KeyPair as Il,stringifyLfvJwks as Pl}from"@ionite/core/server";import{configSourceOriginLogFields as _l,devInMemoryStores as jl,ionite as Et,lfvVault as Ul,mirroredConfig as Ol,secretConfigSource as mi,vaultConfig as Ys}from"@ionite/server";import{createValidators as At}from"@ionite/valibot";import{CONTENT_TYPE_JSON as Bn}from"@ionite/core";import{generateLfvEd25519KeyPair as Ot,signLfvPayload as Qo,stringifyLfvJwks as $t}from"@ionite/core/server";import{ionite as xl,lfvVault as zt}from"@ionite/server";import{createValidators as El}from"@ionite/valibot";var at={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 qt(Be,Ne,Le,ze,De){let Ve=String(Ne.request_id),Je="issued_at"in Ne?Ne:{...Ne,issued_at:Date.now()},Ge=JSON.stringify(Je),We=De??await Qo({privateKeyJwk:Le,requestId:Ve,rawBody:Ge});return new Request(`http://localhost${Be}`,{method:"POST",headers:{...Bn,"X-LFV-Request-Id":Ve,"X-LFV-Key-Id":ze,"X-LFV-Signature":We},body:Ge})}async function fa(Be,Ne,Le){let ze=String(Be.request_id),De=JSON.stringify(Be),Ve=await Qo({privateKeyJwk:Ne,requestId:ze,rawBody:De});return new Response(De,{status:200,headers:{...Bn,"X-LFV-Request-Id":ze,"X-LFV-Key-Id":Le,"X-LFV-Signature":Ve}})}function Al(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function g(){return{tests:[{name:"correctly signed OTP delivery callback is accepted",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-delivery-kid"),ze=$t(Le),De=zt({...at,verifyJwks:ze}),Je={type:"otp",request_id:crypto.randomUUID(),path:at.path,action:"read_secret",otp:"signed-otp"},Ge=await De.handleDelivery(await qt(at.deliveryEndpoint,Je,Ne,"ecv-delivery-kid"));t(Ge.status,503,"a verified OTP with no owner must fail loudly as unroutable"),t((await Ge.json()).error,"callback_unroutable","unroutable deliveries use the callback_unroutable code"),De.close?.()}},{name:"unroutable OTP delivery undoes its replay claim so the same signed callback can be retried",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-unroutable-retry-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De={type:"otp",request_id:crypto.randomUUID(),path:at.path,action:"read_secret",otp:"unroutable-retry",issued_at:Date.now()},Ve=await ze.handleDelivery(await qt(at.deliveryEndpoint,De,Ne,"ecv-unroutable-retry-kid"));t(Ve.status,503,"the first unroutable delivery is a retryable 503");let Je=await ze.handleDelivery(await qt(at.deliveryEndpoint,De,Ne,"ecv-unroutable-retry-kid"));t(Je.status,503,"the same signed callback must be retried, not rejected as a replay"),t((await Je.json()).error,"callback_unroutable","the retry stays unroutable until a waiter or transport can own it"),ze.close?.()}},{name:"read_secret returns confidential material in the TLS action response",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-update-kid"),ze=$t(Le),De=zt({...at,verifyJwks:ze}),Ve=globalThis.fetch;globalThis.fetch=async(Je,Ge)=>{let We=typeof Je==="string"?Je:Je instanceof URL?Je.toString():Je.url,Ke=JSON.parse(String(Ge?.body));if(We.endsWith("/otp")){let Fe=await qt(at.deliveryEndpoint,{type:"otp",request_id:Ke.request_id,path:Ke.path,action:Ke.action,otp:`otp-${Ke.request_id}`},Ne,"ecv-update-kid");return t((await De.handleDelivery(Fe)).status,200,"OTP callback is accepted"),Response.json({status:"pending"},{status:202})}if(We.endsWith("/secrets/request"))return fa({request_id:Ke.request_id,status:"completed",path:Ke.path,data:{data:{marker:"pull-response"},metadata:{path:Ke.path,version:1}}},Ne,"ecv-update-kid");throw Error(`Unexpected LFV request: ${We}`)};try{let Je=await De.getSecret(at.path);t(Je.marker,"pull-response","the action response supplies the secret")}finally{globalThis.fetch=Ve,De.close?.()}}},{name:"unsigned read_secret action response is rejected before the value is published",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-pull-unsigned-kid"),ze=zt({...at,verifyJwks:$t(Le),deliveryTimeout:200}),De=globalThis.fetch;globalThis.fetch=async(Ve,Je)=>{let Ge=typeof Ve==="string"?Ve:Ve instanceof URL?Ve.toString():Ve.url,We=JSON.parse(String(Je?.body));if(Ge.endsWith("/otp"))return await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:We.request_id,path:We.path,action:We.action,otp:`otp-${We.request_id}`},Ne,"ecv-pull-unsigned-kid")),Response.json({status:"pending"},{status:202});return Response.json({request_id:We.request_id,path:We.path,data:{data:{marker:"must-not-publish"},metadata:{path:We.path,version:1}}})};try{let Ve="";try{await ze.getSecret(at.path)}catch(Je){Ve=Je instanceof Error?Je.message:String(Je)}e(Ve.includes("missing signature headers"),"unsigned pull response must fail closed")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"tampered signed read_secret action response is rejected",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-pull-tampered-kid"),ze=zt({...at,verifyJwks:$t(Le),deliveryTimeout:200}),De=globalThis.fetch;globalThis.fetch=async(Ve,Je)=>{let Ge=typeof Ve==="string"?Ve:Ve instanceof URL?Ve.toString():Ve.url,We=JSON.parse(String(Je?.body));if(Ge.endsWith("/otp"))return await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:We.request_id,path:We.path,action:We.action,otp:`otp-${We.request_id}`},Ne,"ecv-pull-tampered-kid")),Response.json({status:"pending"},{status:202});let Ke=await fa({request_id:We.request_id,path:We.path,data:{data:{marker:"original"},metadata:{path:We.path,version:1}}},Ne,"ecv-pull-tampered-kid");return new Response(JSON.stringify({request_id:We.request_id,path:We.path,data:{data:{marker:"tampered"},metadata:{path:We.path,version:1}}}),{status:200,headers:Ke.headers})};try{let Ve="";try{await ze.getSecret(at.path)}catch(Je){Ve=Je instanceof Error?Je.message:String(Je)}e(Ve.includes("signature verification failed"),"tampered pull response must fail closed")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"read_secret action response is bound to the redeem request_id",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-pull-request-id-kid"),ze=zt({...at,verifyJwks:$t(Le),deliveryTimeout:200}),De=globalThis.fetch;globalThis.fetch=async(Ve,Je)=>{let Ge=typeof Ve==="string"?Ve:Ve instanceof URL?Ve.toString():Ve.url,We=JSON.parse(String(Je?.body));if(Ge.endsWith("/otp"))return await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:We.request_id,path:We.path,action:We.action,otp:`otp-${We.request_id}`},Ne,"ecv-pull-request-id-kid")),Response.json({status:"pending"},{status:202});return fa({request_id:crypto.randomUUID(),path:We.path,data:{data:{marker:"wrong-cycle"},metadata:{path:We.path,version:1}}},Ne,"ecv-pull-request-id-kid")};try{let Ve="";try{await ze.getSecret(at.path)}catch(Je){Ve=Je instanceof Error?Je.message:String(Je)}e(Ve.includes("header does not match"),"pull response must match the redeem request_id")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"terminal read_secret denial does not retry the OTP cycle",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-pull-terminal-kid"),ze=zt({...at,verifyJwks:$t(Le),deliveryTimeout:500,retryInterval:10}),De=globalThis.fetch,Ve=0,Je=0;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp"))return Ve+=1,await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,path:Fe.path,action:Fe.action,otp:`otp-${Fe.request_id}`},Ne,"ecv-pull-terminal-kid")),Response.json({status:"pending"},{status:202});return Je+=1,Response.json({error:"permission_denied"},{status:403})};try{let Ge=!1;try{await ze.getSecret(at.path)}catch{Ge=!0}e(Ge,"terminal denial must reject the read"),t(Ve,1,"terminal denial must not mint another OTP"),t(Je,1,"terminal denial must not retry the action")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"404 read_secret is terminal per-read and carries code not_found",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-pull-404-kid"),ze=zt({...at,verifyJwks:$t(Le),deliveryTimeout:500,retryInterval:10}),De=globalThis.fetch,Ve=0,Je=0;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp"))return Ve+=1,await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,path:Fe.path,action:Fe.action,otp:`otp-${Fe.request_id}`},Ne,"ecv-pull-404-kid")),Response.json({status:"pending"},{status:202});return Je+=1,Response.json({error:"not_found"},{status:404})};try{let Ge;try{await ze.getSecret(at.path)}catch(We){Ge=We}e(Ge instanceof Error,"404 must reject the read"),t(Ge.upstreamStatus,404,"the LFV 404 is the upstream status"),t(Ge.status,500,"the client status is not the LFV status"),t(Ge.code,"not_found","404 must classify as not_found"),t(Ve,1,"404 must not mint another OTP"),t(Je,1,"404 must not retry the action")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"forged delivery signature is rejected with 401 invalid_signature",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-forged-kid"),ze=$t(Le),De=zt({...at,verifyJwks:ze}),Je={request_id:crypto.randomUUID(),path:at.path,data:{data:{marker:"forged"},metadata:{path:at.path,version:1}}},Ge=await De.handleDelivery(await qt(at.deliveryEndpoint,Je,Ne,"ecv-forged-kid","AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));t(Ge.status,401,"forged delivery should return 401");let We=await Ge.json();t(We.error,"invalid_signature","error code should be invalid_signature")}},{name:"delivery without signature headers is rejected",fn:async()=>{let{publicJwks:Ne}=await Ot("ecv-missing-kid"),ze=await zt({...at,verifyJwks:$t(Ne)}).handleDelivery(new Request(`http://localhost${at.deliveryEndpoint}`,{method:"POST",headers:Bn,body:JSON.stringify({request_id:crypto.randomUUID(),path:at.path,data:{data:{},metadata:{path:at.path,version:1}}})}));t(ze.status,401,"unsigned delivery should return 401")}},{name:"delivery rejects request_id header/body mismatch",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-mismatch-kid"),ze=$t(Le),De=zt({...at,verifyJwks:ze}),Ve=crypto.randomUUID(),Je=JSON.stringify({request_id:Ve,path:at.path,data:{data:{},metadata:{path:at.path,version:1}}}),Ge=await Qo({privateKeyJwk:Ne,requestId:Ve,rawBody:Je}),We=await De.handleDelivery(new Request(`http://localhost${at.deliveryEndpoint}`,{method:"POST",headers:{...Bn,"X-LFV-Request-Id":crypto.randomUUID(),"X-LFV-Key-Id":"ecv-mismatch-kid","X-LFV-Signature":Ge},body:Je}));t(We.status,401,"mismatched request_id should return 401")}},{name:"delivery is rejected when verifyJwks is not configured",fn:async()=>{let{privateJwk:Ne}=await Ot("ecv-no-jwks-kid"),Le=zt({...at}),De={request_id:crypto.randomUUID(),path:at.path,data:{data:{},metadata:{path:at.path,version:1}}},Ve=await Le.handleDelivery(await qt(at.deliveryEndpoint,De,Ne,"ecv-no-jwks-kid"));t(Ve.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:Ne,publicJwks:Le}=await Ot("ecv-events-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De=[],Ve=ze.subscribeEvents?.((He)=>{De.push({event:He.event,...He.keyId?{keyId:He.keyId}:{}})}),Ge={request_id:crypto.randomUUID(),path:at.path,event:"updated"},We=await ze.handleEvents(await qt(at.eventsEndpoint,Ge,Ne,"ecv-events-kid"));t(We.status,200,"signed events should return 200");let Ke={request_id:crypto.randomUUID(),path:at.path,event:"kms_key_parked",key_id:"k-parked"},Fe=await ze.handleEvents(await qt(at.eventsEndpoint,Ke,Ne,"ecv-events-kid"));t(Fe.status,200,"signed issuer events should return 200"),t(JSON.stringify(De),JSON.stringify([{event:"updated"},{event:"kms_key_parked",keyId:"k-parked"}]),"listeners receive the wire key_id as keyId (metadata-only)"),Ve?.(),ze.close?.()}},{name:"forged events signature is rejected with 401",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-events-forged-kid"),ze=zt({...at,verifyJwks:$t(Le)}),Ve={request_id:crypto.randomUUID(),path:at.path,event:"revoked"},Je=await ze.handleEvents(await qt(at.eventsEndpoint,Ve,Ne,"ecv-events-forged-kid","BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"));t(Je.status,401,"forged events should return 401")}},{name:"replayed delivery callback (same signature) is rejected as a replay",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-replay-kid"),ze=zt({...at,verifyJwks:$t(Le)},{put:async()=>{},take:async()=>{return}}),De={type:"otp",request_id:crypto.randomUUID(),path:at.path,action:"read_secret",otp:"single-use-replay-test",issued_at:Date.now()},Ve=await ze.handleDelivery(await qt(at.deliveryEndpoint,De,Ne,"ecv-replay-kid"));t(Ve.status,200,"first delivery should be accepted");let Je=await ze.handleDelivery(await qt(at.deliveryEndpoint,De,Ne,"ecv-replay-kid"));t(Je.status,401,"an exact replay of the same signed callback must be rejected");let Ge=await Je.json();t(Ge.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:Ne,publicJwks:Le}=await Ot("ecv-stale-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De={request_id:crypto.randomUUID(),path:at.path,issued_at:Date.now()-600000,data:{data:{marker:"stale"},metadata:{path:at.path,version:1}}},Ve=await ze.handleDelivery(await qt(at.deliveryEndpoint,De,Ne,"ecv-stale-kid"));t(Ve.status,401,"a stale callback must be rejected")}},{name:"delivery callbacks reject secret values and accept OTP work items only",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-otp-only-kid"),De=await zt({...at,verifyJwks:$t(Le)}).handleDelivery(await qt(at.deliveryEndpoint,{request_id:crypto.randomUUID(),path:at.path,data:{data:{marker:"must-not-transit"},metadata:{path:at.path,version:1}}},Ne,"ecv-otp-only-kid"));t(De.status,400,"a value-bearing callback is rejected"),t((await De.json()).error,"invalid_request","the callback is rejected as invalid_request")}},{name:"callback fan-out remains scoped by verifyJwks trust identity",fn:async()=>{let Ne=await Ot("ecv-trust-a"),Le=await Ot("ecv-trust-b"),ze={...at,path:"/ecv/lfv/trust-shared"},De={put:async()=>{},take:async()=>{return}},Ve=zt({...ze,verifyJwks:$t(Ne.publicJwks)},De),Je=zt({...ze,verifyJwks:$t(Le.publicJwks)},De);t(Ve.callbackKey,Je.callbackKey,"both sources share one routing key"),e(Ve.trustId!==Je.trustId,"sources preserve distinct trust identities");let Ge=await Ve.handleDelivery(await qt(ze.deliveryEndpoint,{type:"otp",request_id:crypto.randomUUID(),path:ze.path,action:"read_secret",otp:"trust-a-only"},Ne.privateJwk,"ecv-trust-a"));t(Ge.status,200,"source A accepts its trusted OTP"),Ve.close?.(),Je.close?.()}},{name:"failed post-event refresh marks the source degraded (isAvailable() false), not a stale serve",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-degraded-kid"),ze=$t(Le),De=zt({...at,verifyJwks:ze,deliveryTimeout:150,retryInterval:40});t(De.isAvailable?.(),!0,"a newly-created source is available"),await De.handleEvents(await qt(at.eventsEndpoint,{request_id:crypto.randomUUID(),path:at.path,event:"updated"},Ne,"ecv-degraded-kid"));let Ve=Date.now()+3000;while(De.isAvailable?.()!==!1&&Date.now()<Ve)await new Promise((Je)=>setTimeout(Je,25));t(De.isAvailable?.(),!1,"a failed post-event refresh must mark the source degraded via isAvailable()"),De.close?.()}},{name:"cacheMaxAgeSeconds bounds cache hits: a stale entry forces a fresh read",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-maxage-kid"),ze=$t(Le),De=zt({...at,verifyJwks:ze,cacheMaxAgeSeconds:0.05,deliveryTimeout:150,retryInterval:40}),Ve=globalThis.fetch,Je=0;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp"))return await De.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,path:Fe.path,action:Fe.action,otp:`otp-${Fe.request_id}`},Ne,"ecv-maxage-kid")),Response.json({status:"pending"},{status:202});return Je+=1,fa({request_id:Fe.request_id,path:Fe.path,data:{data:{marker:Je===1?"fresh":"refreshed"},metadata:{path:Fe.path,version:Je}}},Ne,"ecv-maxage-kid")};try{t((await De.getSecret(at.path)).marker,"fresh","the initial pull is cached"),await new Promise((Ge)=>setTimeout(Ge,120)),t((await De.getSecret(at.path)).marker,"refreshed","an aged cache entry forces another pull"),t(Je,2,"the action endpoint was called twice")}finally{globalThis.fetch=Ve,De.close?.()}}},{name:"raised kms_slots demand bypasses the cache and is carried on OTP issue and redeem",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-kms-slots-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De=globalThis.fetch,Ve=0,Je=[],Ge=[];globalThis.fetch=async(We,Ke)=>{let Fe=typeof We==="string"?We:We instanceof URL?We.toString():We.url,He=JSON.parse(String(Ke?.body));if(Fe.endsWith("/otp"))return Je.push(He.kms_slots),await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:He.request_id,path:He.path,action:He.action,otp:`otp-${He.request_id}`},Ne,"ecv-kms-slots-kid")),Response.json({status:"pending"},{status:202});return Ge.push(He.kms_slots),Ve+=1,fa({request_id:He.request_id,path:He.path,data:{data:{marker:`grant-v${Ve}`},metadata:{path:He.path,version:Ve}}},Ne,"ecv-kms-slots-kid")};try{let We=[],Ke=await ze.subscribe(at.path,(Ze)=>{We.push(Ze.metadata.version)});t(Ve,1,"the initial subscribe performs one read cycle"),t(Je[0],void 0,"an undeclared demand omits kms_slots from the OTP request"),t(Ge[0],void 0,"an undeclared demand omits kms_slots from the redeem");let Fe,He=await ze.subscribe(at.path,(Ze)=>{Fe=Ze.metadata.version},{kmsSlots:3});t(Ve,2,"a raised demand bypasses the cache and forces a fresh read"),t(Je[1],3,"the raised demand is carried as kms_slots on OTP issuance"),t(Ge[1],3,"the raised demand is carried as kms_slots on the redeem"),t(Fe,2,"the raising subscriber receives the freshly materialized grant");let Xe,Ye=await ze.subscribe(at.path,(Ze)=>{Xe=Ze.metadata.version},{kmsSlots:2});t(Ve,2,"a demand at or below the recorded high-water mark is served from cache"),t(Xe,2,"the cached grant already satisfies the lower demand"),Ke(),He(),Ye()}finally{globalThis.fetch=De,ze.close?.()}}},{name:"kms_park OTP issue and redeem carry key_id, status, and reason and never the secret value",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-kms-park-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De=globalThis.fetch,Ve,Je;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp"))return Ve=Fe.action,await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,path:Fe.path,action:Fe.action,otp:`otp-${Fe.request_id}`},Ne,"ecv-kms-park-kid")),Response.json({status:"pending"},{status:202});return Je={action:Fe.action,key_id:Fe.key_id,status:Fe.status,reason:Fe.reason,value:Fe.value},Response.json({request_id:Fe.request_id,parked:!0,disposition:"cooldown"})};try{let Ge=await ze.parkKmsKey(at.path,{keyId:"k-parked",status:429,reason:"rate limited"});t(Ve,"kms_park","OTP issuance must name the kms_park action"),t(Je?.key_id,"k-parked","the redeem body carries the resolved key id"),t(Je?.status,429,"the redeem body carries the reported status"),t(Je?.reason,"rate limited","the redeem body carries the reported reason"),e(Je?.value===void 0,"the redeem body must never include a key value"),t(Ge.parked,!0,"the source returns the vault park result"),t(Ge.disposition,"cooldown","the source returns the vault disposition")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"kms_add_keys OTP issue and redeem carry issued keys and return assigned ids",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-kms-add-keys-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De=globalThis.fetch,Ve,Je;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp"))return Ve=Fe.action,await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,path:Fe.path,action:Fe.action,otp:`otp-${Fe.request_id}`},Ne,"ecv-kms-add-keys-kid")),Response.json({status:"pending"},{status:202});return Je=Fe.keys,Response.json({request_id:Fe.request_id,keys:[{id:"issued-1"}]})};try{let Ge=await ze.addKmsKeys(at.path,{keys:[{value:"oem-token"}]});t(Ve,"kms_add_keys","OTP issuance must name the kms_add_keys action"),t(JSON.stringify(Je),JSON.stringify([{value:"oem-token"}]),"the redeem body carries issued keys"),t(Ge.keys[0]?.id,"issued-1","the source returns assigned key ids")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"kms_revoke_key OTP issue and redeem carry key_id and reason",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await Ot("ecv-kms-revoke-key-kid"),ze=zt({...at,verifyJwks:$t(Le)}),De=globalThis.fetch,Ve,Je;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp"))return Ve=Fe.action,await ze.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,path:Fe.path,action:Fe.action,otp:`otp-${Fe.request_id}`},Ne,"ecv-kms-revoke-key-kid")),Response.json({status:"pending"},{status:202});return Je={key_id:Fe.key_id,reason:Fe.reason},Response.json({request_id:Fe.request_id,revoked:!0})};try{let Ge=await ze.revokeKmsKey(at.path,{keyId:"k-revoked",reason:"compromise"});t(Ve,"kms_revoke_key","OTP issuance must name the kms_revoke_key action"),t(Je?.key_id,"k-revoked","the redeem body carries the key id"),t(Je?.reason,"compromise","the redeem body carries the reason"),t(Ge.revoked,!0,"the source returns the vault revoke result")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"two LFV sources on one serverUrl|path with different verifyJwks are rejected at apply",fn:async()=>{let Ne=$t((await Ot("ecv-apply-a")).publicJwks),Le=$t((await Ot("ecv-apply-b")).publicJwks),ze="/ecv/lfv/apply-shared",De={id:"ecv-lfv-dual-trust",secrets:{SRC_A:{type:"vault",lfv:{lfvServerUrl:at.lfvServerUrl,clientId:"ecv-apply-client",path:"/ecv/lfv/apply-shared",deliveryEndpoint:"/api/a/lfv/delivery",eventsEndpoint:"/api/a/lfv/events",verifyJwks:Ne}},SRC_B:{type:"vault",lfv:{lfvServerUrl:at.lfvServerUrl,clientId:"ecv-apply-client",path:"/ecv/lfv/apply-shared",deliveryEndpoint:"/api/b/lfv/delivery",eventsEndpoint:"/api/b/lfv/events",verifyJwks:Le}}}},Ve=xl(Al(De),{validators:El(),lfv:{singleProcess:!0}}),Je=!1,Ge="";try{await Ve.ready(2000)}catch(We){Je=!0,Ge=We instanceof Error?We.message:String(We)}Ve.close?.(),e(Je,"a dual-trust LFV config must reject ion.ready() at apply time"),e(Ge.toLowerCase().includes("trust key"),`apply rejection should explain the single-trust-key-per-path invariant (got: ${Ge})`)}}]}}async function pe(){let Be=performance.now(),{tests:Ne}=g(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"LFV callbacks",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}function on(Be,Ne={}){return Object.assign(Error(Be),Ne)}function _a(Be){if(Be.length===0)throw Error("sequenceConfigSource requires at least one step");let Ne=0;return{attempts:()=>Ne,async load(){Ne+=1;let Le=Be[Math.min(Ne-1,Be.length-1)];if(Le===void 0)throw Error("sequenceConfigSource step unexpectedly missing");if(Le instanceof Error)throw Le;if(typeof Le==="function")return await Le();return Le},async subscribe(){return},secret:{}}}async function is(Be){await new Promise((Ne)=>setTimeout(Ne,Be))}function Yt(Be){let Ne=Be,Le;return{async load(){return Ne},async subscribe(ze){return Le=ze,()=>{Le=void 0}},push(ze){Ne=ze,Le?.(ze)},secret:{}}}function hi(Be,Ne){let Le;return{async load(){return await is(Ne),Be},async subscribe(ze){return Le=ze,()=>{Le=void 0}},push(ze){Le?.(ze)},secret:{}}}var os={jwksUri:"http://localhost/workload/certs",issuer:"http://localhost/workload",audience:"http://localhost/workload"};function M(){return[{name:"module readiness re-arms when a module is de-configured and re-configured (§5.1)",fn:async()=>{let Be=Yt({id:"rearm",workload:os}),Ne=Et(Be,{validators:At()});await Ne.ready(500),e(Ne.workload?.isReady?.()===!0,"Expected workload ready after initial config"),Be.push({id:"rearm"}),await is(20),e(Ne.workload?.isReady?.()===!1,"Expected workload.isReady() false after de-configuration");let Le=!1;try{await Ne.workload?.ready?.(50)}catch{Le=!0}e(Le,"Expected re-armed workload.ready(50) to reject while de-configured"),Be.push({id:"rearm",workload:os}),await is(20),e(Ne.workload?.isReady?.()===!0,"Expected workload.isReady() true after re-configuration"),await Ne.workload?.ready?.(200),Ne.close?.()}},{name:"async beforeChange override is awaited and applied (§5.2)",fn:async()=>{let Be=Yt({id:"bc-1"}),Ne=Et(Be,{validators:At(),beforeChange:async(Le)=>(await is(5),{config:{...Le,id:"bc-1-overridden"}})});await Ne.ready(500),t(Ne.config?.id,"bc-1-overridden","Expected async beforeChange override to be applied"),Ne.close?.()}},{name:"awaiting ion.ready() inside beforeChange on initial load fails fast (§5.2)",fn:async()=>{let Be=Yt({id:"deadlock-1"}),Ne=!1,Le=Et(Be,{validators:At(),beforeChange:async()=>{try{await Le.ready(100)}catch{Ne=!0}return}});await Le.ready(500),e(Ne,"Expected ion.ready() inside beforeChange to reject (deadlock guard)"),e(Le.isReady(),"Expected instance to still become ready after beforeChange returns"),Le.close?.()}},{name:"no-argument ion.ready() is bounded by readyTimeoutMs; 0 opts into an indefinite wait",fn:async()=>{let Be=hi({id:"ready-default-bound"},300),Ne=Et(Be,{validators:At(),readyTimeoutMs:50}),Le=!1;try{await Ne.ready()}catch{Le=!0}e(Le,"Expected the no-argument ion.ready() to reject after the readyTimeoutMs bound"),await Ne.ready(0),e(Ne.isReady(),"Expected ion.ready(0) to wait indefinitely and resolve once config applied"),Ne.close?.()}},{name:"config-activated module without required stores is capability-gated; devInMemoryStores() opts in",fn:async()=>{let Be=Et(Yt({id:"stores-gated",ciam:{signingKey:"ecv-test-key"}}),{validators:At()});await Be.ready(500),e(Be.isReady(),"Expected config apply to succeed even with CIAM stores missing");let Ne=Be.readiness().modules.ciam;e(Ne.active,"Expected CIAM to report active (RemoteConfig enables it)"),e(!Ne.ready,"Expected CIAM to report not-ready when its stores are missing"),e(/store/i.test(Ne.reason??""),`Expected a store-naming readiness reason, got: ${Ne.reason}`);let Le=Be.requireReady(["ciam"]);e(!Le.ok,"Expected requireReady([ciam]) to fail while stores are missing"),Be.close?.();let ze=Et(Yt({id:"stores-dev-optin",ciam:{signingKey:"ecv-test-key"}}),{validators:At(),stores:jl()});await ze.ready(500),e(ze.isReady(),"Expected devInMemoryStores() opt-in to become ready"),e(ze.readiness().modules.ciam.ready,"Expected CIAM to be ready with devInMemoryStores()"),ze.close?.()}},{name:"throwing afterChange on initial load rejects ready() (§5.4)",fn:async()=>{let Be=Yt({id:"ac-1",workload:os}),Ne=Et(Be,{validators:At(),afterChange:()=>{throw Error("afterChange boom (initial)")}}),Le=!1;try{await Ne.ready(500)}catch{Le=!0}e(Le,"Expected a throwing initial afterChange to reject ready()"),Ne.close?.()}},{name:"setStores() re-applies config and runs afterChange (§16.2)",fn:async()=>{let Be=0,Ne=!1,Le=Yt({id:"setstores-1",workload:os}),ze=Et(Le,{validators:At(),afterChange:(Ve,Je,Ge)=>{Be+=1,Ne=typeof Ge.setStores==="function"}});await ze.ready(500);let De=Be;e(De>=1,"Expected afterChange to run on the initial apply"),await ze.setStores?.({}),e(Be>De,"Expected afterChange to run again after setStores() re-applies config"),e(Ne,"Expected afterChange to receive a modifiable framework config (with setStores) on the store swap"),ze.close?.()}},{name:"throwing afterChange on a live push keeps last-known-good applied (§5.4)",fn:async()=>{let Be=0,Ne=Yt({id:"aclkg"}),Le=Et(Ne,{validators:At(),afterChange:()=>{if(Be+=1,Be>=2)throw Error("afterChange boom (live)")}});await Le.ready(500),e(Le.isReady(),"Expected ready after first apply"),Ne.push({id:"aclkg",workload:os}),await is(20),e(Le.isReady(),"Expected instance to stay ready (LKG) after a throwing live afterChange"),e(Le.config?.workload!==void 0,"Expected the pushed config to still apply despite afterChange throwing"),Le.close?.()}},{name:"a live update that changes the immutable RemoteConfig.id is rejected and keeps last-known-good (§2.4)",fn:async()=>{let Be=Yt({id:"immutable-id",workload:os}),Ne=Et(Be,{validators:At()});await Ne.ready(500),t(Ne.config?.id,"immutable-id","Expected the initial id to apply"),Be.push({id:"different-id",workload:os}),await is(20),e(Ne.isReady(),"Expected the instance to stay ready after a rejected id change"),t(Ne.config?.id,"immutable-id","Expected the immutable id change to be rejected (LKG retained)"),e(Ne.configStatus?.lastError!==void 0,"Expected configStatus.lastError to record the rejected immutable-id change"),Ne.close?.()}},{name:"empty OAuth client secrets are rejected at config apply; live updates keep last-known-good",fn:async()=>{let Be=Yt({id:"empty-ciam-client-secret",ciam:{providers:{oidc:{type:"oidc",clientId:"client-id",clientSecret:""}}}}),Ne=Et(Be,{validators:At()}),Le=!1;try{await Ne.ready(500)}catch{Le=!0}e(Le,"Expected an empty CIAM provider clientSecret to reject the initial config"),e(Ne.configStatus.lastError?.message.includes("ciam.providers.oidc")===!0,`Expected the initial config error to name ciam.providers.oidc, got: ${Ne.configStatus.lastError?.message}`),Ne.close?.();let ze=Yt({id:"empty-sso-client-secret"}),De=Et(ze,{validators:At()});await De.ready(500),ze.push({id:"empty-sso-client-secret",sso:{clientId:"client-id",clientSecret:""}}),await is(20),e(De.isReady(),"Expected the instance to stay ready after rejecting an empty SSO clientSecret"),e(De.config?.sso===void 0,"Expected last-known-good config to remain applied"),e(De.configStatus.lastError?.message.includes("sso")===!0,`Expected the live-update config error to name sso, got: ${De.configStatus.lastError?.message}`),De.close?.()}},{name:"setStores() and a concurrent live push are serialized; the push is reflected (§5.1)",fn:async()=>{let Be=Yt({id:"s51",workload:os}),Ne=Et(Be,{validators:At(),beforeChange:async(ze)=>(await is(50),{config:ze})});await Ne.ready(1000),e(Ne.config?.capabilitiez!==!0,"Expected capabilitiez unset before the push");let Le=Ne.setStores?.({});Be.push({id:"s51",workload:os,capabilitiez:!0}),await Le,await is(150),t(Ne.config?.capabilitiez,!0,"Expected the concurrent live push to be reflected after setStores (serialized, not reverted)"),Ne.close?.()}},{name:"a wait-mode push that lands during a slow initial load wins over the stale load (§5.2)",fn:async()=>{let Be=hi({id:"race",workload:os},80),Ne=Et(Be,{validators:At()});Be.push({id:"race",workload:os,capabilitiez:!0}),await Ne.ready(500),t(Ne.config?.capabilitiez,!0,"Expected the wait-mode push (v2) to be the applied config"),await is(120),t(Ne.config?.capabilitiez,!0,"Expected the stale initial load to be skipped (push retained)"),Ne.close?.()}},{name:"a secrets source config unchanged across an sso-only re-apply keeps a stable, ready source (§7.9)",fn:async()=>{let Be=Yt({id:"memo",workload:os,secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),Ne=Et(Be,{validators:At()});await Ne.ready(500),e(Ne.secrets?.isReady?.()===!0,"Expected the secrets module ready after initial apply");let Le=Ne.secrets?.getSecretsSource("primary");Be.push({id:"memo",workload:os,sso:{clientId:"c",clientSecret:"s",authority:"http://localhost/idp"},secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),await is(30),e(Ne.secrets?.isReady?.()===!0,"Expected the secrets module to stay ready after an sso-only change");let ze=Ne.secrets?.getSecretsSource("primary");e(Le===ze,"Expected the unchanged secrets source handle to be reused (memoized) across apply"),Ne.close?.()}}]}function Jn(Be){return{type:"vault",getFullSecret:async()=>({data:Be,metadata:{version:1}})}}function gi(Be){return{type:"vault",getSecret:async()=>Be}}async function ja(Be,Ne){try{await Be()}catch(Le){if(Ne&&!Ne(Le))throw Error(`Threw an unexpected error: ${Le instanceof Error?Le.message:String(Le)}`);return Le}throw Error("Expected the operation to throw, but it resolved")}function N(){return[{name:"RemoteConfig credential wins over a framework-set secrets credential (§2.1)",fn:async()=>{let Be=At(),Ne={},Le={...Be,secrets:{...Be.secrets,validateSourceConfig:(Ve,Je)=>{Ne[Ve]=Je}}},ze=Yt({id:"merge-1",secrets:{primary:{type:"vault",url:"http://localhost/vault",token:"REMOTE-TOKEN"}}}),De=Et(ze,{validators:Le,secrets:{primary:{token:"CODE-TOKEN"}}});await De.ready(500),t(Ne.primary?.token,"REMOTE-TOKEN","Expected the merged secrets config to use the RemoteConfig token, not the framework token"),De.close?.()}},{name:"unimplemented secrets source types (aws/gcp/dev) fail at config apply, not first access (§4.3)",fn:async()=>{for(let Be of["aws","gcp","dev"]){let Ne=Yt({id:`unimpl-${Be}`,secrets:{x:{type:Be}}}),Le=Et(Ne,{validators:At()}),ze=!1;try{await Le.ready(300)}catch{ze=!0}e(ze,`Expected secrets source type "${Be}" to fail readiness at config apply`),Le.close?.()}}},{name:"vault config-source rejects an empty payload as recoverable and a wrong-shape payload as invalid (§2.2)",fn:async()=>{let Be=Ys({vault:Jn({}),path:"ion/config"});await ja(()=>Be.load(),(De)=>De instanceof Error&&(De.name==="VaultConfigWaitingForSecretError"||De.code==="not_found"||/empty|missing/i.test(De.message)));let Ne=Ys({vault:Jn({foo:1}),path:"ion/config"});await ja(()=>Ne.load(),(De)=>De instanceof Error&&/invalid payload|not a remoteconfig/i.test(De.message));let ze=await Ys({vault:Jn({data:{id:"cfg-ok"}}),path:"ion/config"}).load();t(ze.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 Be=Jn({data:{id:"polling-ttl"}});await ja(async()=>Ys({vault:Be,path:"ion/config",ttl:599999}),(ze)=>ze instanceof Error&&/10 minutes|600000/.test(ze.message));let Le=await Ys({vault:Be,path:"ion/config",ttl:600000}).load();t(Le.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 Be=mi(gi({type:"not-a-real-type"}),"boot/locator");await ja(()=>Be.load(),(Le)=>Le instanceof Error&&/invalid payload|unknown type/i.test(Le.message));let Ne=mi(gi("vault"),"boot/locator");await ja(()=>Ne.load(),(Le)=>Le instanceof Error&&/invalid payload|expected an object/i.test(Le.message))}},{name:"config-source origin names the backend without credentials and rides the RemoteConfig applied line",fn:async()=>{let Le=Ys({url:"http://ops:ORIGIN-SECRET-MUST-NOT-BE-LOGGED@vault.test/v1/secret/data/?token=ORIGIN-SECRET-MUST-NOT-BE-LOGGED#frag",token:"ORIGIN-SECRET-MUST-NOT-BE-LOGGED",path:"ion/apps/demo/config"});t(Le.origin?.type,"vault","direct vault source reports type vault"),t(Le.origin?.transport,"http","a direct url reads over HTTP"),t(Le.origin?.url,"http://vault.test/v1/secret/data","url keeps only scheme/host/path"),t(Le.origin?.path,"ion/apps/demo/config","origin names the RemoteConfig document path");let ze=Ys({websocket:{url:"ws://vault.test/api/vault/v1/ws",token:"ORIGIN-SECRET-MUST-NOT-BE-LOGGED",clientId:"app-1"},path:"ion/apps/demo/config"});t(ze.origin?.transport,"websocket","a websocket-only source reads over the socket"),t(ze.origin?.url,"ws://vault.test/api/vault/v1/ws","websocket url is reported");let De=Ys({lfv:{lfvServerUrl:"http://vault.test/api/lfv/",clientId:"lfv-client-1",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events",verifyJwks:'{"keys":[]}'},path:"ion/apps/demo/config"});t(De.origin?.transport,"lfv","an LFV-capable source reads through LFV"),t(De.origin?.url,"http://vault.test/api/lfv","LFV server url is reported");for(let tt of[Le,ze,De])e(!JSON.stringify(tt.origin).includes("ORIGIN-SECRET-MUST-NOT-BE-LOGGED"),"origin must never carry credentials"),await tt.secret.close?.();let Ge=Ol({from:{type:"vault",origin:{transport:"lfv",url:"http://eci.test/api/eci/lfv"},getFullSecret:async()=>({data:{},metadata:{version:1}})},fromPath:"ion/source/demo",to:{type:"vault",origin:{transport:"websocket",url:"ws://vault.test/api/vault/v1/ws"},getFullSecret:async()=>({data:{},metadata:{version:1}})},path:"ion/apps/demo/mirror"});t(Ge.origin?.type,"mirror","mirrored source reports type mirror"),t(Ge.origin?.transport,"websocket","mirror transport is the destination transport"),t(Ge.origin?.path,"ion/apps/demo/mirror","mirror path is the destination document"),t(Ge.origin?.from?.type,"vault","upstream kind is reported under from"),t(Ge.origin?.from?.transport,"lfv","upstream transport is reported under from"),t(Ge.origin?.from?.path,"ion/source/demo","upstream path is reported under from");let We=_l(Ge.origin);t(We["ion.config.source.from.url"],"http://eci.test/api/eci/lfv","from.* flattens");let Ke=[],Fe={trace:()=>{},debug:()=>{},info:(tt,...Qe)=>{if(Qe[0]&&typeof Qe[0]==="object")Ke.push([tt,Qe[0]])},warn:()=>{},error:()=>{}},He=Yt({id:"origin-demo",workload:os});He.origin={type:"vault",transport:"http",url:"http://vault.test/v1/secret/data",path:"ion/apps/demo/config"};let Xe=Et(He,{validators:At(),log:Fe});await Xe.ready(500);let Ye=Ke.find(([tt])=>tt==="RemoteConfig applied.");e(Ye!==void 0,"Expected a RemoteConfig applied. log line");let Ze=Ye[1];t(Ze["ion.config.source.type"],"vault","applied line names the source type"),t(Ze["ion.config.source.transport"],"http","applied line names the transport"),t(Ze["ion.config.source.url"],"http://vault.test/v1/secret/data","applied line names the url"),t(Ze["ion.config.source.path"],"ion/apps/demo/config","applied line names the document path"),e(!JSON.stringify(Ke).includes("ORIGIN-SECRET-MUST-NOT-BE-LOGGED"),"no info line may carry the credential"),Xe.close?.()}},{name:"a configured beforeChange re-applies even when the effective config is unchanged (§2.3)",fn:async()=>{let Be=0,Ne=Yt({id:"normalize-me",workload:os}),Le=Et(Ne,{validators:At(),beforeChange:(De)=>({config:{...De,id:"normalized"}}),afterChange:()=>{Be+=1}});await Le.ready(500);let ze=Be;e(ze>=1,"Expected at least one apply on the initial load"),Ne.push({id:"normalize-me-2",workload:os}),await is(30),e(Be>ze,"Expected beforeChange-present pipeline to re-apply on a normalized push"),Le.close?.()}},{name:"secrets module stays not-ready while a declared source lacks a ConfigSource type (§4.5)",fn:async()=>{let Be=Yt({id:"sec-ready",workload:os}),Ne=Et(Be,{validators:At(),secrets:{needsRemote:{}}});await Ne.ready(500),e(Ne.isReady(),"Expected the instance to be ready (config applied) on initial load"),e(Ne.secrets?.isReady?.()===!1,"Expected the secrets module to be not-ready while a declared source has no ConfigSource type"),Be.push({id:"sec-ready",workload:os,secrets:{needsRemote:{type:"vault",url:"http://localhost/vault"}}}),await is(30),e(Ne.secrets?.isReady?.()===!0,"Expected the secrets module to become ready once the ConfigSource supplies the source type"),Ne.close?.()}},{name:"IdP assignment validates its destination and claims contract while live rejection preserves last-known-good",fn:async()=>{let Be={idpTokenUrl:"https://idp.example.test/token",clientId:"ecv-client",clientSecret:"ecv-secret"},Ne={clientId:"ecv-sso",clientSecret:"ecv-secret",claimsAsEntitlements:{claims:["groups"]}},Le=[{id:"idp-missing-destination",workload:Be,sso:Ne,iam:{users:{assignment:"idp"}}},{id:"idp-workload",workload:Be,iam:{workloads:{assignment:"idp"}}},{id:"idp-missing-claims",workload:Be,sso:{clientId:"ecv-sso",clientSecret:"ecv-secret",accessRequests:{url:"https://idp.example.test"}}},{id:"idp-magic-link",workload:Be,ciam:{providers:{magic:{type:"magic-link",claimsAsEntitlements:{claims:["groups"]},accessRequests:{url:"https://idp.example.test"}}}}}];for(let Ve of Le){let Je=Et(Yt(Ve),{validators:At()});await ja(()=>Je.ready(500),(Ge)=>Ge instanceof Error),Je.close?.()}let ze=Yt({id:"idp-last-known-good"}),De=Et(ze,{validators:At()});await De.ready(500),ze.push({id:"idp-last-known-good",workload:Be,sso:Ne,iam:{users:{assignment:"idp"}}}),await is(30),e(De.isReady(),"a rejected live IdP assignment must leave the instance ready"),e(De.config?.iam===void 0,"a rejected live IdP assignment must retain last-known-good config"),e(De.configStatus.lastError?.message.includes("accessRequests")===!0,`the rejected live update should identify accessRequests, got ${De.configStatus.lastError?.message}`),De.close?.()}}]}function L(){return[{name:"retries recoverable missing config and then becomes ready",fn:async()=>{let Be=_a([on("Waiting for vault config secret at ion/test",{code:"not_found",status:404,sourceType:"vault"}),{id:"retry-ok"}]),Ne=Et(Be,{validators:At(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await Ne.ready(250),t(Be.attempts(),2,"Expected one retry after recoverable missing config"),Ne.close?.()}},{name:"retries an LFV 404 not_found then becomes ready (§2.1 transport parity)",fn:async()=>{let{privateJwk:Be,publicJwks:Ne}=await Il("ecv-lfv-404-retry-kid"),Le=Ul({...at,verifyJwks:Pl(Ne),deliveryTimeout:500,retryInterval:10}),ze=Ys({path:at.path,vault:Le}),De=0,Ve={attempts:()=>De,load:async()=>(De+=1,ze.load()),subscribe:async()=>{return},secret:ze.secret},Je=globalThis.fetch;globalThis.fetch=async(We,Ke)=>{let Fe=typeof We==="string"?We:We instanceof URL?We.toString():We.url,He=JSON.parse(String(Ke?.body));if(Fe.endsWith("/otp")){if(De<=1)return Response.json({error:"not_found"},{status:404});return await Le.handleDelivery(await qt(at.deliveryEndpoint,{type:"otp",request_id:He.request_id,path:He.path,action:He.action,otp:`otp-${He.request_id}`},Be,"ecv-lfv-404-retry-kid")),Response.json({status:"pending"},{status:202})}if(Fe.endsWith("/secrets/request"))return fa({request_id:He.request_id,status:"completed",path:He.path,data:{data:{id:"lfv-404-retry-ok"},metadata:{path:He.path,version:1}}},Be,"ecv-lfv-404-retry-kid");throw Error(`Unexpected LFV request: ${Fe}`)};let Ge=Et(Ve,{validators:At(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});try{await Ge.ready(250),e(Ge.isReady(),"Expected LFV 404 not_found to retry and then become ready"),t(Ve.attempts(),2,"Expected one retry after an LFV 404"),t(Ge.config?.id,"lfv-404-retry-ok","Expected the recovered LFV config to apply")}finally{globalThis.fetch=Je,Ge.close?.(),Le.close?.()}}},{name:"fails fast for unrecoverable auth errors",fn:async()=>{let Be=_a([on("Vault returned 403 Forbidden (auth error)",{status:403,sourceType:"vault"}),{id:"should-not-load"}]),Ne=Et(Be,{validators:At(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}}),Le=!1;try{await Ne.ready(250)}catch{Le=!0}await is(10),e(Le,"Expected auth failure to reject readiness"),t(Be.attempts(),1,"Expected auth failure not to retry"),Ne.close?.()}},{name:"retries unclassified (unknown) errors instead of failing fast (§2.1)",fn:async()=>{let Be=_a([Error("unexpected config loader bug"),{id:"unknown-retry-ok"}]),Ne=Et(Be,{validators:At(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await Ne.ready(250),e(Ne.isReady(),"Expected an unknown error to retry and then become ready"),t(Be.attempts(),2,"Expected one retry after an unknown (retry-by-default) error"),Ne.close?.()}},{name:"retries transient transport codes (EAI_AGAIN) then becomes ready (§2.1)",fn:async()=>{let Be=_a([on("getaddrinfo EAI_AGAIN vault.internal",{code:"EAI_AGAIN",sourceType:"vault"}),on("getaddrinfo EAI_AGAIN vault.internal",{code:"EAI_AGAIN",sourceType:"vault"}),{id:"eai-ok"}]),Ne=Et(Be,{validators:At(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}});await Ne.ready(500),e(Ne.isReady(),"Expected EAI_AGAIN to retry and then become ready"),t(Be.attempts(),3,"Expected two retries after EAI_AGAIN before success"),Ne.close?.()}},{name:"fails fast for a structured 4xx (403) load error (§2.1)",fn:async()=>{let Be=_a([on("Vault returned 403 Forbidden",{status:403,sourceType:"vault"}),{id:"should-not-load"}]),Ne=Et(Be,{validators:At(),configRetry:{initialDelayMs:1,maxDelayMs:1,jitterRatio:0,loadTimeoutMs:50}}),Le=!1;try{await Ne.ready(250)}catch{Le=!0}await is(10),e(Le,"Expected a 403 load to fail fast"),t(Be.attempts(),1,"Expected a provably-unrecoverable 403 not to retry"),Ne.close?.()}},{name:"handler returns 503 immediately while the instance is not ready",fn:async()=>{let Be=_a([async()=>(await is(100),{id:"slow-config"})]),Ne=Et(Be,{validators:At()});e(!Ne.isReady(),"Expected instance to start not ready");let Le=await Ne.handler(new Request("http://localhost/api/ionite/auth/user"));t(Le.status,503,"Expected handler to return 503 immediately when not ready"),await Ne.ready(250),e(Ne.isReady(),"Expected instance to be ready after config applied"),Ne.close?.()}}]}import{devInMemoryStores as ql,ionite as $l}from"@ionite/server";import{createValidators as Ml}from"@ionite/valibot";var Nl=new Set(["apiKey","clientSecret","headers","privateKey","remoteConfig","signingKey","token","value"]);function Ll(Be){let Ne;return{load:async()=>Be,subscribe:async(Le)=>(Ne=Le,()=>{Ne=void 0}),push:(Le)=>Ne?.({...Be,...Le}),secret:{}}}function Gn(Be,Ne="description"){if(e(Be!==void 0,`${Ne} must not be undefined (JSON drops it, breaking deep equality)`),e(typeof Be!=="function",`${Ne} must not expose a function`),Array.isArray(Be)){Be.forEach((ze,De)=>Gn(ze,`${Ne}[${De}]`));return}if(!Be||typeof Be!=="object")return;let Le=Object.getPrototypeOf(Be);e(Le===Object.prototype||Le===null,`${Ne} must be a plain object, not a class instance`);for(let[ze,De]of Object.entries(Be))e(!Nl.has(ze),`${Ne}.${ze} must not expose a sensitive field`),Gn(De,`${Ne}.${ze}`)}function er(){let Be=Ll({id:"describe-ecv",sso:{authority:"https://idp.example.test",clientId:"describe-app",clientSecret:"never-describe-sso",redirectUri:"/api/ionite/auth/callback",jwksUri:"https://idp.example.test/certs"},ciam:{providers:{example:{type:"oidc",clientId:"describe-customer",clientSecret:"never-describe-ciam",privateKey:"never-describe-key"}}},workload:{incoming:[{issuer:"https://idp.example.test",jwksUri:"https://idp.example.test/certs",audience:"describe-api",claimsAsEntitlements:{claims:["scope"],expand:!0}}],outgoing:{vault:{clientId:"describe-workload",clientSecret:"never-describe-workload",idpTokenUrl:"https://idp.example.test/token"}}},iam:{url:"https://iam.example.test/scim/v2",users:{enabled:!0},groups:{enabled:!0}},authz:{oaa:{url:"https://oaa.example.test",apiKey:"never-describe-oaa",providerId:"provider",datasourceId:"datasource"}},otel:{name:"describe",apiKey:"never-describe-otel",headers:{Authorization:"never-describe-header"},console:"off",traces:!1,metrics:!1,logs:!1}});return{ion:$l(Be,{basePath:"/api/ionite",stores:ql(),validators:Ml()}),reapply:Be.push}}function fi(){return{tests:[{name:"describe snapshots are JSON data and exclude sensitive fields",fn:async()=>{let{ion:Be}=er();try{await Be.ready(500);let Ne=Be.describe();t(Be.sso?.describe().ready,Ne.modules.sso.ready,"module describe readiness must match the current root readiness snapshot"),Gn(Ne)}finally{Be.close()}}},{name:"root and active module handles expose describe",fn:async()=>{let{ion:Be}=er();try{await Be.ready(500);let Ne=[Be,Be.sso,Be.ciam,Be.iam,Be.workload,Be.otel,Be.authz,Be.authz.oaa];for(let Le of Ne)e(Le!==void 0,"Expected configured module handle"),e(typeof Le.describe==="function","Expected configured handle.describe()"),Gn(Le.describe())}finally{Be.close()}}},{name:"module projections survive repeated config applies and describe named children by name",fn:async()=>{let{ion:Be,reapply:Ne}=er();try{await Be.ready(500);let Le=Be.describe();Ne({capabilitiez:!0}),await new Promise((We)=>setTimeout(We,20));let ze=Be.describe();t(JSON.stringify(ze.modules),JSON.stringify(Le.modules),"module descriptions must be stable across an identical re-apply");let De=Be.workload?.describe();e(De!==void 0,"Expected workload description"),t(De.incoming.trustedIdps[0]?.audience,"describe-api","incoming trust is described"),t(De.outgoing.length,1,"aggregate workload describes each outgoing client"),t(De.outgoing[0]?.name,"vault","outgoing client is described under its declared name"),t(De.outgoing[0]?.clientId,"describe-workload","outgoing client carries the public id"),t(De.outgoing[0]?.authMethod,"client_secret","auth method is named, never valued");let Ve=Be.workload.vault;t(JSON.stringify(Ve?.describe()),JSON.stringify(De.outgoing[0]),"named client handle and aggregate report the same projection");let Je=Be.otel?.describe();e(Je!==void 0,"Expected otel description"),t(Je.name,"describe","otel projection reads the RemoteConfig block"),t(Je.signals.traces.enabled,!1,"otel projection reflects signal policy");let Ge=Be.authz.describe();t(De.incoming.trustedIdps[0]?.claimsAsEntitlements?.claims[0],"scope","incoming trust describes the claims block"),t(De.incoming.trustedIdps[0]?.claimsAsEntitlements?.capable,!0,"workload claims are capable without a session store"),t(Ge.oaa?.providerId,"provider","authz projection embeds the OAA summary")}finally{Be.close()}}}]}}function z(){return[{name:"readyPath defaults to basePath + /readyz (/api/ionite/readyz) (§17.3)",fn:async()=>{t(Xa({}),"/api/ionite/readyz","Expected the default readyPath to derive from /api/ionite"),t(Xa({basePath:"/api/ionite/"}),"/api/ionite/readyz","Expected a trailing-slash basePath to normalize")}},{name:"readyPath derives from a custom basePath (§17.3)",fn:async()=>{t(Xa({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()=>{t(Xa({readyPath:"/api/readyz",basePath:"/api/ionite"}),"/api/readyz","Expected an explicit readyPath to override basePath derivation")}}]}import{ACCEPT_SCIM as Bt,CANONICAL_RISK_LEVELS as zl,CONTENT_TYPE_SCIM as ya,ION_AUTHZ_GROUP_SCHEMA as yi,ION_SCIM_USER_EXTENSION_URN as vs}from"@ionite/core";import{createValidators as Dl}from"@ionite/valibot";var tr=Dl(),Wn="urn:ietf:params:scim:schemas:core:2.0:User",rn="urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",Vl="urn:ietf:params:scim:api:messages:2.0:ListResponse",wi="ecid:ecv.ionite.test:01J9F3Z8QK7M2N4P6R8T0V2W4X";function Tt(Be,Ne){return e(typeof Be==="object"&&Be!==null&&!Array.isArray(Be),`${Ne} must be an object`),Be}function As(Be,Ne){return e(typeof Be==="string",`${Ne} must be a string`),Be}function vi(Be,Ne){return e(typeof Be==="number",`${Ne} must be a number`),Be}function Ua(Be,Ne){return e(Array.isArray(Be)&&Be.every((Le)=>typeof Le==="string"),`${Ne} must be a string array`),Be}function Xs(Be,Ne){return e(Array.isArray(Be),`${Ne} must be an array`),Be}function sr(Be,Ne){let Le=Be[yi];if(Le===void 0)return;let ze=Tt(Le,`${Ne} authz extension`);if(ze.type!==void 0)e(ze.type==="group"||ze.type==="role"||ze.type==="permission",`${Ne} authz extension type must be group, role, or permission`);if(ze.requestable!==void 0)e(typeof ze.requestable==="boolean",`${Ne} authz extension requestable must be a boolean`);if(ze.riskLevel!==void 0)e(typeof ze.riskLevel==="string",`${Ne} authz extension riskLevel must be a string`);if(ze.requestUrl!==void 0)e(typeof ze.requestUrl==="string",`${Ne} authz extension requestUrl must be a string`),new URL(ze.requestUrl);if(ze.description!==void 0)e(typeof ze.description==="string",`${Ne} authz extension description must be a string`)}function ar(Be,Ne,Le,ze){let De=Tt(Ne.meta,`Created ${Le} meta`),Ve=As(De.location,`Created ${Le} meta.location`),Je=Be.headers.get("Location");e(Je===Ve,`Location header must equal meta.location for created ${Le}`),e(new URL(Ve).pathname.endsWith(`/${Le}/${encodeURIComponent(ze)}`),`Created ${Le} location must address the new resource id`)}var bi={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"},[rn]:{department:"Platform",costCenter:"ENG-001",organization:"ionite"},[vs]:{ecid:wi,accountClass:"daily"}},testGroup:{displayName:"ECV Test Group",externalId:"ecv-test-group-001"}};async function Xt(Be){let Ne=typeof Be.ion==="function"?Be.ion():Be.ion;if(Ne!==null&&Ne!==void 0){if(await Ne.ready(Be.timeout??15000),!Ne.workload)throw Error("Workload management is required for IAM tests. Ionite instance provided but workload is not configured.");let ze=Ne.workload;return`Bearer ${typeof ze.getServerToken==="function"?await ze.getServerToken():await ze.getToken()}`}return`Bearer ${await o(Ra(Be.ecvUrl,"IAM validation ecvUrl when ion is not provided"),U)}`}async function Bl(Be){return`Bearer ${await o(Ra(Be.ecvUrl,"IAM validation ecvUrl when ion is not provided"))}`}async function ki(Be,Ne){return s("IAM Users Endpoint Schema",async()=>{let Le=await Xt(Be),ze=await Ne(`${Be.scimPath}/Users`,{headers:{Authorization:Le,...Bt}});e(ze.status===200,`Expected 200 OK, got ${ze.status}`);let De=Tt(await ze.json(),"SCIM users response"),Ve=Ua(De.schemas,"Response schemas"),Je=vi(De.totalResults,"Response totalResults"),Ge=Xs(De.Resources,"Response Resources");return e(Ve.includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"),"Response missing SCIM ListResponse schema"),{details:{totalResults:Je,resourceCount:Ge.length}}})}async function Jl(Be,Ne){return s("IAM Groups Endpoint Schema",async()=>{let Le=await Xt(Be),ze=await Ne(`${Be.scimPath}/Groups`,{headers:{Authorization:Le,...Bt}});e(ze.status===200,`Expected 200 OK, got ${ze.status}`);let De=Tt(await ze.json(),"SCIM groups response"),Ve=Ua(De.schemas,"Response schemas"),Je=vi(De.totalResults,"Response totalResults"),Ge=De.Resources===void 0?[]:Xs(De.Resources,"Response Resources");for(let We of Ge)sr(Tt(We,"SCIM group resource"),"SCIM group resource");return e(Ve.includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"),"Response missing SCIM ListResponse schema"),{details:{totalResults:Je,resourceCount:Ge.length}}})}async function Si(Be,Ne){if(!Be.testUser)return{...c("IAM Create User","No test user configured"),userId:void 0};let Le=Be.testUser,ze=await s("IAM Create User",async()=>{let De=await Xt(Be),Ve={schemas:[Wn,rn,...Le[vs]?[vs]:[]],...Le},Je=await Ne(`${Be.scimPath}/Users`,{method:"POST",headers:{Authorization:De,...ya,...Bt},body:JSON.stringify(Ve)});e(Je.status===201,`Expected 201 Created, got ${Je.status}`);let Ge=Tt(await Je.json(),"Created SCIM user");r(Ge,tr.iam.user);let We=Ua(Ge.schemas,"Created user schemas"),Ke=As(Ge.id,"Created user id"),Fe=As(Ge.userName,"Created user userName");if(ar(Je,Ge,"Users",Ke),e(We.includes(Wn),"Created user missing SCIM User schema"),Le[rn])e(We.includes(rn),"Created user missing enterprise extension schema");if(Le[vs]){e(We.includes(vs),"Created user missing ionite User extension schema");let He=Tt(Ge[vs],"Created user ionite extension");e(He.ecid===wi,"Created user must round-trip its ECID"),e(He.accountClass==="daily","Created user must round-trip its accountClass")}return{details:{userId:Ke,userName:Fe}}});return{...ze,userId:ze.details?.userId}}async function Ti(Be,Ne,Le){return s("IAM Get User",async()=>{let ze=await Xt(Be),De=await Ne(`${Be.scimPath}/Users/${Le}`,{headers:{Authorization:ze,...Bt}});e(De.status===200,`Expected 200 OK, got ${De.status}`);let Ve=Tt(await De.json(),"Fetched SCIM user");r(Ve,tr.iam.user);let Je=As(Ve.id,"Fetched user id"),Ge=As(Ve.userName,"Fetched user userName");e(Je===Le,`User ID mismatch: expected ${Le}, got ${Je}`);let We=Be.testUser?.[rn];if(We){let Fe=Tt(Ve["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"Fetched user enterprise extension");if(We.department!==void 0)e(Fe.department===We.department,`Expected department ${We.department}, got ${String(Fe.department)}`);if(We.costCenter!==void 0)e(Fe.costCenter===We.costCenter,`Expected costCenter ${We.costCenter}, got ${String(Fe.costCenter)}`)}let Ke=Be.testUser?.[vs];if(Ke){let Fe=Tt(Ve[vs],"Fetched user ionite extension");e(Fe.ecid===Ke.ecid,"Fetched user must preserve its ECID"),e(Fe.accountClass===Ke.accountClass,"Fetched user must preserve its accountClass")}return{details:{userId:Je,userName:Ge}}})}async function Ci(Be,Ne,Le){return s("IAM Update User",async()=>{let ze=await Xt(Be),De={schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:"ECV Test User (Updated)"}]},Ve=await Ne(`${Be.scimPath}/Users/${Le}`,{method:"PATCH",headers:{Authorization:ze,...ya,...Bt},body:JSON.stringify(De)});e(Ve.status===200,`Expected 200 OK, got ${Ve.status}`);let Je=Tt(await Ve.json(),"Updated SCIM user"),Ge=As(Je.id,"Updated user id"),We=As(Je.displayName,"Updated user displayName");return e(We==="ECV Test User (Updated)","displayName was not updated"),{details:{userId:Ge,displayName:We}}})}async function Ri(Be,Ne,Le){return s("IAM Delete User",async()=>{let ze=await Xt(Be),De=await Ne(`${Be.scimPath}/Users/${Le}`,{method:"DELETE",headers:{Authorization:ze}});e(De.status===204,`Expected 204 No Content, got ${De.status}`);let Ve=await Ne(`${Be.scimPath}/Users/${Le}`,{headers:{Authorization:ze,...Bt}});return e(Ve.status===404,`Expected 404 after deletion, got ${Ve.status}`),{details:{userId:Le,deleted:!0}}})}async function xi(Be,Ne){if(!Be.testGroup)return{...c("IAM Create Group","No test group configured"),groupId:void 0};let Le=await s("IAM Create Group",async()=>{let ze=await Xt(Be),De={schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],...Be.testGroup},Ve=await Ne(`${Be.scimPath}/Groups`,{method:"POST",headers:{Authorization:ze,...ya,...Bt},body:JSON.stringify(De)});e(Ve.status===201,`Expected 201 Created, got ${Ve.status}`);let Je=Tt(await Ve.json(),"Created SCIM group");r(Je,tr.iam.group);let Ge=As(Je.id,"Created group id"),We=As(Je.displayName,"Created group displayName");return sr(Je,"Created SCIM group"),ar(Ve,Je,"Groups",Ge),{details:{groupId:Ge,displayName:We}}});return{...Le,groupId:Le.details?.groupId}}async function Ei(Be,Ne,Le){return s("IAM Delete Group",async()=>{let ze=await Xt(Be),De=await Ne(`${Be.scimPath}/Groups/${Le}`,{method:"DELETE",headers:{Authorization:ze}});return e(De.status===204,`Expected 204 No Content, got ${De.status}`),{details:{groupId:Le,deleted:!0}}})}async function Ai(Be,Ne){return s("IAM Users Endpoint Requires Authentication",async()=>{let Le=await Ne(`${Be.scimPath}/Users`,{headers:Bt});return e(Le.status===401,`Expected 401 Unauthorized for Users, got ${Le.status}`),{details:{usersRequiresAuth:!0}}})}async function Gl(Be,Ne){return s("IAM Groups Endpoint Requires Authentication",async()=>{let Le=await Ne(`${Be.scimPath}/Groups`,{headers:Bt});return e(Le.status===401,`Expected 401 Unauthorized for Groups, got ${Le.status}`),{details:{groupsRequiresAuth:!0}}})}async function Ii(Be,Ne){return s("IAM Users Endpoint Denies Unauthorized (403)",async()=>{let Le=await Bl(Be),ze=await Ne(`${Be.scimPath}/Users`,{headers:{Authorization:Le,...Bt}});return e(ze.status===403,`Expected 403 Forbidden for an authenticated-but-unauthorized token, got ${ze.status}`),{details:{deniedByDefault:!0}}})}function Pi(Be,Ne){return[{name:"IAM discovery endpoints require authentication",fn:async()=>{for(let Le of["ServiceProviderConfig","ResourceTypes","Schemas"]){let ze=await Ne(`${Be.scimPath}/${Le}`,{headers:Bt});e(ze.status===401,`Expected 401 from unauthenticated ${Le}, got ${ze.status}`)}}},{name:"IAM ServiceProviderConfig advertises OAuth bearer auth and filtering",fn:async()=>{let Le=await Ne(`${Be.scimPath}/ServiceProviderConfig`,{headers:{Authorization:await Xt(Be),...Bt}});e(Le.status===200,`Expected 200 from ServiceProviderConfig, got ${Le.status}`);let ze=Tt(await Le.json(),"ServiceProviderConfig"),De=Tt(ze.filter,"ServiceProviderConfig.filter");e(De.supported===!0,"ServiceProviderConfig must advertise filter support");let Ve=Xs(ze.authenticationSchemes,"ServiceProviderConfig.authenticationSchemes").map((Ge)=>Tt(Ge,"ServiceProviderConfig authentication scheme"));e(Ve.some((Ge)=>Ge.type==="oauthbearertoken"),"ServiceProviderConfig must advertise OAuth bearer token authentication");let Je=Tt(ze.meta,"ServiceProviderConfig.meta");new URL(As(Je.location,"ServiceProviderConfig.meta.location"))}},{name:"IAM Schemas exposes the ionite ECID User extension",fn:async()=>{let Le=await Xt(Be),ze=await Ne(`${Be.scimPath}/Schemas`,{headers:{Authorization:Le,...Bt}});e(ze.status===200,`Expected 200 from Schemas, got ${ze.status}`);let De=Tt(await ze.json(),"Schemas response");e(Ua(De.schemas,"Schemas response schemas").includes(Vl),"Schemas response must be a SCIM ListResponse");let Ve=Xs(De.Resources,"Schemas Resources").map((Fe)=>Tt(Fe,"Schema resource"));e(Ve.some((Fe)=>Fe.id===Wn),"Schemas must include the core User schema"),e(Ve.some((Fe)=>Fe.id===vs),"Schemas must include the ionite User extension");let Je=await Ne(`${Be.scimPath}/Schemas/${encodeURIComponent(vs)}`,{headers:{Authorization:Le,...Bt}});e(Je.status===200,`Expected 200 from ionite User Schema, got ${Je.status}`);let Ge=Tt(await Je.json(),"Ionite User Schema");e(Ge.id===vs,"Schema detail must identify the ionite User extension");let We=Xs(Ge.attributes,"Ionite User Schema attributes").map((Fe)=>Tt(Fe,"Ionite User Schema attribute")),Ke=We.find((Fe)=>Fe.name==="ecid");e(Ke?.type==="string","Ionite User Schema must define ecid as a string"),e(Ke.caseExact===!0,"Ionite User Schema must define ecid as case-exact"),e(We.some((Fe)=>Fe.name==="accountClass"),"Ionite User Schema must define accountClass")}},{name:"IAM ResourceTypes binds the ionite extension to Users",fn:async()=>{let Le=await Ne(`${Be.scimPath}/ResourceTypes`,{headers:{Authorization:await Xt(Be),...Bt}});e(Le.status===200,`Expected 200 from ResourceTypes, got ${Le.status}`);let ze=Tt(await Le.json(),"ResourceTypes response"),Ve=Xs(ze.Resources,"ResourceTypes Resources").map((Ge)=>Tt(Ge,"ResourceType resource")).find((Ge)=>Ge.id==="User");e(Ve!==void 0,"ResourceTypes must include User");let Je=Xs(Ve.schemaExtensions,"User schemaExtensions").map((Ge)=>Tt(Ge,"User schema extension"));e(Je.some((Ge)=>Ge.schema===vs),"User ResourceType must reference the ionite User extension")}},{name:"IAM Users rejects a malformed ECID at the HTTP boundary",fn:async()=>{let Le=await Ne(`${Be.scimPath}/Users`,{method:"POST",headers:{Authorization:await Xt(Be),...ya,...Bt},body:JSON.stringify({schemas:[Wn,vs],userName:`invalid-ecid-${Date.now()}@example.test`,active:!0,[vs]:{ecid:"not-an-ecid",accountClass:"daily"}})});e(Le.status===400,`Expected 400 for malformed ECID, got ${Le.status}`)}}]}async function me(Be){let Ne=performance.now(),Le={...bi,...Be},ze=a(Le),De=[],Ve=typeof Be.ion==="function"?Be.ion():Be.ion;if((Ve===null||Ve===void 0)&&!Be.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(Ve&&!Ve.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}};De.push(await Ai(Le,ze)),De.push(await Ii(Le,ze));for(let He of Pi(Le,ze))De.push(await s(He.name,async()=>{await He.fn();return}));De.push(await ki(Le,ze));let Je=await Si(Le,ze);if(De.push(Je),Je.passed&&Je.userId)De.push(await Ti(Le,ze,Je.userId)),De.push(await Ci(Le,ze,Je.userId)),De.push(await Ri(Le,ze,Je.userId));if(Be.testGroup){De.push(await Gl(Le,ze)),De.push(await Jl(Le,ze));let He=await xi(Le,ze);if(De.push(He),He.passed&&He.groupId)De.push(await Ei(Le,ze,He.groupId))}let Ge=performance.now()-Ne,We=De.filter((He)=>He.passed).length,Ke=De.filter((He)=>!He.passed).length,Fe=De.filter((He)=>He.details?.skipped).length;return{suite:"IAM",passed:Ke===0,tests:De,duration:Ge,summary:{total:De.length,passed:We-Fe,failed:Ke,skipped:Fe}}}function f(Be){let Ne={...bi,...Be},Le=a(Ne),ze,De,Ve;return{tests:[{name:"users endpoint requires authentication",fn:async()=>{let We=await Ai(Ne,Le);if(!We.passed)throw Error(We.error)}},{name:"users endpoint denies an authenticated-but-unauthorized token (403)",fn:async()=>{let We=await Ii(Ne,Le);if(!We.passed)throw Error(We.error)}},{name:"users endpoint returns proper SCIM schema",fn:async()=>{let We=await ki(Ne,Le);if(!We.passed)throw Error(We.error)}},...Pi(Ne,Le),{name:"can create a user",fn:async()=>{let We=await Si(Ne,Le);if(!We.passed)throw Error(We.error);ze=We.userId}},{name:"can get a user by ID",fn:async()=>{if(!ze)throw Error("No user ID from create test");let We=await Ti(Ne,Le,ze);if(!We.passed)throw Error(We.error)}},{name:"can update a user",fn:async()=>{if(!ze)throw Error("No user ID from create test");let We=await Ci(Ne,Le,ze);if(!We.passed)throw Error(We.error)}},{name:"can delete a user",fn:async()=>{if(!ze)throw Error("No user ID from create test");let We=await Ri(Ne,Le,ze);if(!We.passed)throw Error(We.error)}}],ext:{createGroupsOutboundTests:()=>[{name:"can create a group in external IAM",fn:async()=>{let We=await xi(Ne,Le);if(!We.passed)throw Error(We.error);De=We.groupId}},{name:"can delete a group in external IAM",fn:async()=>{if(!De)throw Error("No group ID from create test");let We=await Ei(Ne,Le,De);if(!We.passed)throw Error(We.error)}}],createGroupsInboundTests:(We=!1)=>{let Ke=Be.groupsInboundPath??`${Be.scimPath}/Groups`;return[{name:"inbound endpoint requires authentication",fn:async()=>{let Fe=await Le(Ke,{headers:Bt});if(Fe.status!==401)throw Error(`Expected 401 Unauthorized, got ${Fe.status}`)}},{name:"inbound endpoint returns proper SCIM schema",fn:async()=>{let Fe=await Xt(Ne),He=await Le(Ke,{headers:{Authorization:Fe,...Bt}});if(He.status!==200)throw Error(`Expected 200 OK, got ${He.status}`);let Xe=Tt(await He.json(),"Inbound groups response");if(!Ua(Xe.schemas,"Inbound response schemas").includes("urn:ietf:params:scim:api:messages:2.0:ListResponse"))throw Error("Response missing SCIM ListResponse schema")}},{name:"defaults IAM-sourced entitlement creation to 501 unless advertised",fn:async()=>{if(We)return;let Fe=await Xt(Ne),He={schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],displayName:"ECV Inbound Test Group",externalId:"ecv-inbound-test-group-001"},Xe=await Le(Ke,{method:"POST",headers:{Authorization:Fe,...ya,...Bt},body:JSON.stringify(He)});if(Xe.status!==501)throw Error(`Expected 501 Not Implemented, got ${Xe.status}`)}},{name:"ServiceProviderConfig advertises filter support (§11.3)",fn:async()=>{let Fe=await Xt(Ne),He=Ke.replace(/\/Groups$/,""),Xe=await Le(`${He}/ServiceProviderConfig`,{headers:{Authorization:Fe,...Bt}});if(Xe.status!==200)throw Error(`Expected 200 OK from ServiceProviderConfig, got ${Xe.status}`);let Ye=Tt(await Xe.json(),"ServiceProviderConfig");if(Tt(Ye.filter,"ServiceProviderConfig.filter").supported!==!0)throw Error("ServiceProviderConfig must advertise filter.supported: true (filtering IS implemented)")}},{name:"authz Group schema advertises IAM-writable riskLevel suggestions",fn:async()=>{let Fe=await Xt(Ne),He=Ke.replace(/\/Groups$/,""),Xe=await Le(`${He}/Schemas/${encodeURIComponent(yi)}`,{headers:{Authorization:Fe,...Bt}});e(Xe.status===200,`Expected 200 from authz Group Schema, got ${Xe.status}`);let Ye=Tt(await Xe.json(),"Authz Group Schema"),Ze=Xs(Ye.attributes,"Authz Group Schema attributes").map((st)=>Tt(st,"Authz Group Schema attribute")),tt=Ze.find((st)=>st.name==="riskLevel");e(tt?.type==="string","riskLevel must be a string"),e(tt.mutability==="readWrite","riskLevel must be writable by IAM"),e(JSON.stringify(Ua(tt.canonicalValues,"riskLevel canonicalValues"))===JSON.stringify(zl),"riskLevel canonicalValues must advertise the SDK suggestions in order");let Qe=Ze.find((st)=>st.name==="attributes");e(Qe?.type==="complex","custom attributes must be a complex SCIM attribute"),e(Qe.mutability==="readWrite","custom attributes must be writable by IAM")}}]},createGroupsInboundCreateTests:()=>{let We=Be.groupsInboundPath??`${Be.scimPath}/Groups`;return[{name:"can receive entitlement creation from IAM provider",fn:async()=>{let Ke=await Xt(Ne),Fe=await Le(We,{method:"POST",headers:{Authorization:Ke,...ya,...Bt},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(Fe.status!==201)throw Error(`Expected 201 Created, got ${Fe.status}`);let He=Tt(await Fe.json(),"Created inbound SCIM group"),Xe=As(He.id,"Created inbound group id");As(He.displayName,"Created inbound group displayName"),sr(He,"Created inbound SCIM group"),ar(Fe,He,"Groups",Xe),Ve=Xe}},{name:"externalId eq filter round-trips the IAM-created entitlement (§11.3)",fn:async()=>{if(!Ve)throw Error("No group ID from create test");let Ke=await Xt(Ne),Fe=encodeURIComponent('externalId eq "ecv-inbound-test-group-001"'),He=await Le(`${We}?filter=${Fe}`,{headers:{Authorization:Ke,...Bt}});e(He.status===200,`Expected 200 OK, got ${He.status}`);let Xe=Tt(await He.json(),"Filtered groups response"),Ye=Xs(Xe.Resources??[],"Filtered Resources").map((Ze)=>Tt(Ze,"Filtered resource")).find((Ze)=>Ze.externalId==="ecv-inbound-test-group-001");e(Ye!==void 0,"externalId eq filter did not return the IAM-created entitlement")}},{name:"rejects a malformed IAM-created entitlement PATCH with 400 (§10.2)",fn:async()=>{if(!Ve)throw Error("No group ID from create test");let Ke=await Xt(Ne),Fe=await Le(`${We}/${Ve}`,{method:"PATCH",headers:{Authorization:Ke,...ya,...Bt},body:JSON.stringify({schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"displayName",value:12345}]})});e(Fe.status===400,`Expected 400 for a malformed group PATCH, got ${Fe.status}`)}},{name:"can receive IAM-created entitlement deletion",fn:async()=>{if(!Ve)throw Error("No group ID from create test");let Ke=await Xt(Ne),Fe=await Le(`${We}/${Ve}`,{method:"DELETE",headers:{Authorization:Ke}});e(Fe.status===204,`Expected 204 No Content, got ${Fe.status}`)}}]}}}}import{ACCEPT_SCIM as _i,CONTENT_TYPE_JSON as Wl,CONTENT_TYPE_SCIM as Kl}from"@ionite/core";import{baseCustomerToScim as Fl,baseWorkloadToScim as Hl}from"@ionite/core/server";import{createManagedIamDelta as bs,defineCatalog as Yl,InMemoryAccessCatalogStore as Oi,InMemoryCustomerStore as Xl,InMemoryUserStore as qi,InMemoryWorkloadIdentityStore as Zl,ION_AUTHZ_GROUP_SCHEMA as Zs,ION_SCIM_CUSTOMER_SCHEMA as Ql,ION_SCIM_USER_EXTENSION_URN as ji,ION_SCIM_WORKLOAD_SCHEMA as eu,ionite as tu,materializeCodeCatalogEntity as su,ScimOutboundClient as $i,silentLogger as cs,workload as Ui}from"@ionite/server";import{createValidators as au}from"@ionite/valibot";var nu="urn:ietf:params:scim:api:messages:2.0:ListResponse";function It(Be="https://iam.example.test/scim/v2"){let Ne=new Map,Le=new Set,ze=new Map,De=new Set,Ve=new Map,Je=new Set,Ge=new Map,We=new Set,Ke=[],Fe=0,He,Xe,Ye=404,Ze=(ct,ft)=>new Response(JSON.stringify(ft),{status:ct,headers:Kl}),tt={Users:"User",Groups:"Group",Workloads:"Workload",Customers:"Customer"},Qe={Users:Ne,Groups:ze,Workloads:Ve,Customers:Ge},st={Users:Le,Groups:De,Workloads:Je,Customers:We},et={Users:"u",Groups:"g",Workloads:"w",Customers:"c"},nt=(ct,ft,ut,Rt={})=>{let jt=`${Be}/${ct}/${encodeURIComponent(ft)}`,Ft=`${new URL(Be).origin}/request-access/${encodeURIComponent(ft)}`,Os=ct==="Groups"?{...Rt[Zs]??{},requestUrl:Ft}:void 0;return{...Rt,id:ft,externalId:ut,meta:{resourceType:tt[ct],location:jt},...Os?{[Zs]:Os}:{}}},dt=(ct,ft,ut)=>Ze(200,{schemas:[nu],totalResults:ut?1:0,Resources:ut?[nt(ct,ut,ft)]:[]});return{baseUrl:Be,fetchImpl:async(ct,ft)=>{let ut=new URL(ct),Rt=(ft?.method??"GET").toUpperCase(),jt=ut.pathname.split("/").filter(Boolean),Ft=jt[jt.length-2],Os=["Users","Groups","Workloads","Customers"],_o=Os.includes(Ft)?Ft:Os.includes(jt[jt.length-1])?jt[jt.length-1]:jt[jt.length-1],rs=Os.includes(_o)?_o:void 0,Lr=ut.searchParams.get("filter")??void 0,Ha=ft?.body?JSON.parse(String(ft.body)):void 0;if(Ke.push({method:Rt,path:ut.pathname,filter:Lr,body:Ha}),Rt==="GET"&&rs&&ut.pathname.endsWith(`/${rs}`)){if(Xe!==void 0)return Ze(Xe,{detail:"lookup rejected"});let ms=Lr?.match(/externalId eq "([^"]*)"/i)?.[1];e(ms!==void 0,"destination lookups must be by externalId filter only");let na=Qe[rs].get(ms);return dt(rs,ms,na)}if(Rt==="POST"&&rs&&ut.pathname.endsWith(`/${rs}`)){let ms=String(Ha?.externalId??"");Fe+=1;let na=`${et[rs]}_${Fe}`;return Qe[rs].set(ms,na),st[rs].add(na),Ze(201,nt(rs,na,ms,Ha))}if(Rt==="PUT"&&rs){let ms=decodeURIComponent(jt[jt.length-1]??"");if(!st[rs].has(ms))return Ze(Ye,{detail:"resource not found"});let Ya=String(Ha?.externalId??"");return Qe[rs].set(Ya,ms),Ze(200,nt(rs,ms,Ya,Ha))}if(Rt==="DELETE"&&rs){let ms=decodeURIComponent(jt[jt.length-1]??""),na=st[rs],Ya=Qe[rs];if(!na.delete(ms))return Ze(Ye,{detail:"resource not found"});for(let[Nd,Ld]of Ya)if(Ld===ms)Ya.delete(Nd);return new Response(null,{status:204})}if(Rt==="PATCH"&&_o==="Groups"){let ms=decodeURIComponent(jt[jt.length-1]??"");if(!De.has(ms))return Ze(Ye,{detail:"group not found"});if(He!==void 0)return Ze(He,{detail:"patch rejected"});return Ze(200,{id:ms})}return Ze(404,{detail:"not found"})},calls:Ke,seedUser(ct,ft){Ne.set(ct,ft),Le.add(ft)},seedGroup(ct,ft){ze.set(ct,ft),De.add(ft)},countCalls(ct){return Ke.filter(ct).length},failGroupPatchWith(ct){He=ct},failLookupWith(ct){Xe=ct},missingResourceWith(ct){Ye=ct}}}function nr(){let Be=new Map,Ne=[],Le=[],ze=(De,Ve)=>`${De}\x00${Ve}`;return{callbacks:{async read(De,Ve){return Be.get(ze(De,Ve))},async write(De,Ve,Je){Be.set(ze(De,Ve),Je),Ne.push({resourceType:De,externalId:Ve,reference:Je})},async clear(De,Ve){Be.delete(ze(De,Ve)),Le.push({resourceType:De,externalId:Ve})}},get:(De,Ve)=>Be.get(ze(De,Ve)),set:(De,Ve,Je)=>Be.set(ze(De,Ve),Je),writes:Ne,clears:Le}}function hs(Be,Ne=nr()){return{client:new $i({baseUrl:Be.baseUrl,getToken:async()=>"test-token",receipts:Ne.callbacks,log:cs,fetchImpl:Be.fetchImpl}),receipts:Ne}}var es={isReady:()=>!0,ready:async()=>{return},getServerToken:async()=>"test-token",getToken:async(Be)=>`token-for-${Be??"server"}`};function ks(){return{userStore:new qi,catalogStore:new Oi,customerStore:new Xl,workloadIdentityStore:new Zl}}var Zt={users:"patch",workloads:"patch",customers:"patch"};function ou(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function y(){return{tests:[{name:"SCIM egress requests JSON and preserves an unexpected HTML error title for Ops",fn:async()=>{let Ne=[],Le={trace:()=>{},debug:()=>{},info:()=>{},warn:(We,...Ke)=>Ne.push({message:We,fields:Ke[0]&&typeof Ke[0]==="object"?Ke[0]:void 0}),error:()=>{}},ze=nr(),De=null,Je=await new $i({baseUrl:"https://iam.example.test/scim/v2",getToken:async()=>"test-token",receipts:ze.callbacks,log:Le,fetchImpl:async(We,Ke)=>(De=new Headers(Ke?.headers).get("Accept"),new Response("<html><title>Cloud edge unavailable</title><body>opaque details</body></html>",{status:503,headers:{"Content-Type":"text/html; charset=UTF-8"}}))}).resolveProviderId("user","u-edge-failure");e(Je==="error","an HTML destination error remains a degraded lookup, not an absence"),e(De===_i.Accept,`SCIM request must send ${_i.Accept}, got ${De}`);let Ge=Ne.find((We)=>We.fields?.["ion.operation"]==="iam.delta.resolve");e(typeof Ge?.fields?.detail==="string"&&Ge.fields.detail.includes("Cloud edge unavailable")&&!Ge.fields.detail.includes("<html>"),"the warning carries the HTML title without dumping the page")}},{name:"resolveProviderId resolves a destination id STRICTLY by externalId filter",fn:async()=>{let Ne=It();Ne.seedUser("u-alice","u_42");let{client:Le,receipts:ze}=hs(Ne),De=await Le.resolveProviderId("user","u-alice");e(typeof De==="object"&&De.found==="u_42","resolves the destination id returned by the externalId filter lookup");let Ve=Ne.calls.find((Ge)=>Ge.method==="GET"&&Ge.path.endsWith("/Users"));e(Ve?.filter?.includes('externalId eq "u-alice"')===!0,"lookup used the externalId filter");let Je=ze.get("user","u-alice");e(Je?.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 Ne=It(),{client:Le,receipts:ze}=hs(Ne),De=await Le.resolveProviderId("group","g-new",{displayName:"New Group"});e(typeof De==="object"&&De.found.startsWith("g_"),"a missing resource is created and its id returned"),e(Ne.countCalls((Je)=>Je.method==="POST"&&Je.path.endsWith("/Groups"))===1,"exactly one create is issued when the destination has no match");let Ve=ze.get("group","g-new");e(Ve?.providerId===(typeof De==="object"?De.found:void 0),"the provider id is cached"),e(Ve?.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 Ne=It();Ne.seedGroup("g-existing","g_44");let{client:Le,receipts:ze}=hs(Ne);await Le.resolveProviderId("group","g-existing");let De=ze.get("group","g-existing");e(De?.providerId==="g_44","the lookup caches the provider id"),e(De?.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 Ne=It();Ne.seedGroup("g-eng","g_7");let{client:Le}=hs(Ne);await Le.resolveProviderId("group","g-eng"),await Le.resolveProviderId("group","g-eng"),e(Ne.countCalls((ze)=>ze.method==="GET"&&ze.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 Ne=It();Ne.seedUser("u-alice","u_1"),Ne.seedGroup("g-eng","g_1");let{client:Le}=hs(Ne);await Le.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"});let ze=Ne.calls.find((Ve)=>Ve.method==="PATCH"&&Ve.path.endsWith("/Groups/g_1"));e(ze!==void 0,"a membership PATCH is issued against the resolved group id");let De=ze?.body?.Operations;e(De?.[0]?.op==="add","the patch adds membership"),e(De?.[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 Ne=It();Ne.seedUser("u-alice","u_1"),Ne.seedGroup("g-eng","g_real");let{client:Le,receipts:ze}=hs(Ne);ze.set("group","g-eng",{providerId:"g_stale",requestUrl:"https://old-iam.example.test/request/g_stale"}),await Le.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"}),e(Ne.calls.some((Ve)=>Ve.method==="PATCH"&&Ve.path.endsWith("/Groups/g_stale")),"the first PATCH targets the stale id and is rejected (404)"),e(Ne.calls.some((Ve)=>Ve.method==="PATCH"&&Ve.path.endsWith("/Groups/g_real")),"after self-heal the retry targets the freshly re-resolved id");let De=ze.get("group","g-eng");e(De?.providerId==="g_real","the row receipt is repaired to the correct provider id"),e(ze.clears.some((Ve)=>Ve.resourceType==="group"&&Ve.externalId==="g-eng"),"the stale row receipt is cleared before the externalId re-filter"),e(De?.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 Ne=It(),{client:Le}=hs(Ne),ze={groupExternalId:"g-eng",userExternalId:"u-alice",op:"add",groupCreate:{displayName:"Eng"},userCreate:{userName:"alice"}};await Le.patchGroupMembership(ze),await Le.patchGroupMembership(ze),e(Ne.countCalls((De)=>De.method==="POST"&&De.path.endsWith("/Users"))===1,"the user is created at most once across repeated emits"),e(Ne.countCalls((De)=>De.method==="POST"&&De.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 Ne=It(),{client:Le}=hs(Ne);await Promise.all([Le.upsertResource("group","g-eng",{displayName:"Engineering v1"}),Le.upsertResource("group","g-eng",{displayName:"Engineering v2"})]),e(Ne.countCalls((De)=>De.method==="POST"&&De.path.endsWith("/Groups"))===1,"concurrent upserts create the destination Group once");let ze=Ne.calls.find((De)=>De.method==="PUT"&&De.path.includes("/Groups/"));e(ze?.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 Ne=It();Ne.seedGroup("g-reviewers","g_10");let{client:Le,receipts:ze}=hs(Ne),De=await Le.upsertResource("group","g-reviewers",{schemas:["urn:ietf:params:scim:schemas:core:2.0:Group"],externalId:"g-reviewers",displayName:"Reviewers",[Zs]:{type:"group",requestable:!0,requestUrl:"https://app.example.test/must-not-egress",description:"Review access",resources:["project:42"]}});e(De==="pushed","the full Group upsert is accepted");let Je=Ne.calls.find((We)=>We.method==="PUT"&&We.path.endsWith("/Groups/g_10"))?.body?.[Zs];e(Je?.requestable===!0,"the requestable flag is included in the PUT body"),e(Je?.resources?.[0]==="project:42","resource bindings are included in the PUT body"),e(Je?.requestUrl===void 0,"the destination-owned request URL is stripped from the PUT body");let Ge=ze.get("group","g-reviewers");e(Ge?.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 Ne=It(),Le={id:"vault_secret_create",name:"Create secret",description:"Internal permission composed into requestable resource Groups",requestable:!1,assignableSubjectTypes:["user"]},De=bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...ks(),fetchImpl:Ne.fetchImpl});await De.onEntitlementUpsert(Le,"permission"),await De.onGrant({subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test",name:"Alice",email:"alice@example.test"},subjectType:"user",targetType:"permission",target:Le}),e(Ne.calls.length===0,"non-requestable definitions are Authz/OAA building blocks and never reach IAM delta")}},{name:"entitlement egress projects the union-merged row, not the sparse materialized snapshot",fn:async()=>{let Ne=It(),Le=Yl({groups:[{id:"ops",name:"Ops",description:"Ops access (code-declared)",requestable:!0,riskLevel:"elevated",assignableSubjectTypes:["user"]}]}),ze=ks(),De=Le.groups.get("ops");if(e(De!==void 0,"the code catalog declares the ops group"),!De)return;await su("group",De,ze.catalogStore.groups);let Ve=await ze.catalogStore.groups.get("ops");if(e(Ve!==void 0&&Ve.riskLevel===void 0,"materialization persists structure, never the advisory riskLevel seed"),!Ve)return;await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...ze,codeCatalog:Le,fetchImpl:Ne.fetchImpl}).onEntitlementUpsert(Ve,"group");let We=Ne.calls.find((Fe)=>(Fe.method==="POST"||Fe.method==="PUT")&&Fe.path.includes("/Groups"));e(We!==void 0,"the requestable code entitlement egresses");let Ke=We?.body?.[Zs];e(Ke?.riskLevel==="elevated","delta egress carries the code-seeded riskLevel, not the sparse-row default")}},{name:"entitlement metadata upsert reapplies observed roots after the destination PUT",fn:async()=>{let Ne=It();Ne.seedGroup("reviewers","g_10");let ze={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,assignableSubjectTypes:["user"],groups:[{id:"employees",name:"Employees",description:"All employees",requestable:!0,assignableSubjectTypes:["user"]}]},De={listByTarget:async()=>({items:[{subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test"},subjectType:"user",targetType:"group",target:ze}],total:1,count:1,size:1,page:1,pages:1})};await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...ks(),accessStore:De,fetchImpl:Ne.fetchImpl}).onEntitlementUpsert(ze,"group");let Ge=Ne.calls.findIndex((Fe)=>Fe.method==="PUT"&&Fe.path.endsWith("/Groups/g_10")),We=Ne.calls.findIndex((Fe)=>Fe.method==="PATCH"&&Fe.path.endsWith("/Groups/g_10")),Ke=Ne.calls[Ge]?.body;e(Ge>=0,"the entitlement definition is replaced"),e(Ke?.members?.[0]?.value?.startsWith("u_")===!0,"the PUT carries the resolved existing member"),e(Ke?.members?.[0]?.type==="User","the PUT carries the full-sync member type"),e(Ke?.members?.some((Fe)=>Fe.type==="Group"&&Fe.value?.startsWith("g_"))===!0,"the PUT carries nested Group membership through destination provider ids"),e(We>Ge,"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 Ne=It();Ne.seedGroup("reviewers","g_10");let Le={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,assignableSubjectTypes:["user"]},ze=1002,De=Array.from({length:ze},(Xe,Ye)=>({subject:{id:`user-${Ye}`,externalId:`u-${Ye}`,userName:`user${Ye}@example.test`},subjectType:"user",targetType:"group",target:Le})),Ve=[],Je={listByTarget:async(Xe,Ye,Ze)=>{let tt=Ze?.start??0,Qe=Ze?.limit??ze;Ve.push(tt);let st=De.slice(tt,tt+Qe);return{total:ze,count:st.length,items:st,size:Qe,page:Math.floor(tt/Math.max(Qe,1))+1,pages:Math.ceil(ze/Math.max(Qe,1))}}};await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...ks(),accessStore:Je,fetchImpl:Ne.fetchImpl}).onEntitlementUpsert(Le,"group"),e(Ve.includes(1000),"the emitter requests roots beyond the first page");let Fe=Ne.calls.find((Xe)=>Xe.method==="PUT"&&Xe.path.endsWith("/Groups/g_10"))?.body?.members??[];e(Fe.length===ze,"the definition PUT carries members from every page");let He=Ne.countCalls((Xe)=>Xe.method==="PATCH"&&Xe.path.endsWith("/Groups/g_10"));e(He===ze,"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 Ne=It(),Le=bs(cs),ze=ks(),De=Le.update({...Zt,url:Ne.baseUrl},{workload:es,...ze,fetchImpl:Ne.fetchImpl}),Ve="u-terminated-alice",Je={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",dataSteward:"users@example.test"};await ze.userStore.upsert(Je),await De.onUserUpsert(Je);let We=Ne.calls.find((Fe)=>Fe.method==="POST"&&Fe.path.endsWith("/Users"))?.body;e(We?.active===!1,"a terminated user egresses with active:false"),e(We?.schemas?.includes(ji)===!0,"the User egress declares the ionite ECID extension schema");let Ke=We?.[ji];e(Ke?.ecid===Je.ecid,"the User egress carries the ECID"),e(Ke?.accountClass===Je.accountClass,"the User egress carries accountClass"),e(Ke?.attributes?.dataSteward==="users@example.test","the User delta body carries flat AppDev custom attributes"),await De.onUserDelete("u-terminated-alice"),e(Ne.calls.some((Fe)=>Fe.method==="DELETE"&&Fe.path.includes("/Users/u_")),"the managed emitter deletes the destination User by its resolved provider id"),e(await De.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 Ne=It(),{client:Le,receipts:ze}=hs(Ne),De=await Le.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}]});e(De==="pushed","an unknown User is created with its full body");let Ve=Ne.calls.find((We)=>We.method==="POST"&&We.path.endsWith("/Users"));e(Ve?.body?.emails?.[0]?.value==="alice@example.test","full User attributes are present on create-on-empty");let Je=await Le.deleteResource("user","u-alice");e(Je==="pushed","the destination User delete is accepted"),e(Ne.calls.some((We)=>We.method==="DELETE"&&We.path.includes("/Users/u_")),"the resolved provider User is deleted");let Ge=ze.get("user","u-alice");e(Ge===void 0,"the deleted User mapping is removed")}},{name:"deleteResource treats a confirmed absent destination resource as idempotent success",fn:async()=>{let Ne=It(),{client:Le}=hs(Ne),ze=await Le.deleteResource("group","g-absent");e(ze==="pushed","confirmed absence is a successful converged delete"),e(Ne.countCalls((De)=>De.method==="POST"||De.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 Ne=It();Ne.missingResourceWith(410),Ne.seedGroup("g-reviewers","g_real");let{client:Le,receipts:ze}=hs(Ne);ze.set("group","g-reviewers",{providerId:"g_stale"});let De=await Le.deleteResource("group","g-reviewers");e(De==="pushed","the stale mapping converges to a successful delete"),e(Ne.calls.some((Ve)=>Ve.method==="DELETE"&&Ve.path.endsWith("/Groups/g_stale")),"the first delete detects the stale cached id"),e(Ne.calls.some((Ve)=>Ve.method==="DELETE"&&Ve.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 Ne=It();await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...ks(),fetchImpl:Ne.fetchImpl}).onGrant({subject:{id:"alice",externalId:"u-alice",userName:"alice@example.test"},subjectType:"user",targetType:"role",target:{id:"reviewer",name:"Reviewer",requestable:!0}});let De=Ne.calls.find((Je)=>Je.method==="POST"&&Je.path.endsWith("/Groups")),Ve=De?.body?.[Zs];e(Ve?.type==="role","the role definition is represented as a typed SCIM Group"),e(De?.body?.schemas?.includes(Zs)===!0,"the typed Group extension is declared in schemas"),e(De?.body?.externalId==="reviewer","the App-owned catalog id is the outbound SCIM externalId"),e(Ne.calls.some((Je)=>Je.method==="PATCH"&&Je.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 Ne=It(),Le=ks();await Le.workloadIdentityStore.upsert({id:"billing-worker",clientId:"billing-worker-client",externalId:"w-billing-worker",displayName:"Billing worker"}),await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...Le,mapWorkloadToScim:(We)=>({...Hl(We),ownerTeam:"payments"}),fetchImpl:Ne.fetchImpl}).onGrant({subject:"billing-worker",subjectType:"workload",targetType:"role",target:{id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0,assignableSubjectTypes:["user"]}});let Ve=Ne.calls.find((We)=>We.method==="POST"&&We.path.endsWith("/Workloads"));e(Ve?.body?.userName==="billing-worker-client","the workload registry supplies the native Workload destination body"),e(Ve?.body?.schemas?.includes(eu)===!0,"the Workload body declares the ionite Workload schema URN"),e(Ve?.body?.ownerTeam==="payments","the AppDev workload projection reaches IAM delta");let Ge=Ne.calls.find((We)=>We.method==="PATCH")?.body?.Operations?.[0]?.value?.[0];e(Ge?.type==="Workload","the membership PATCH matches the full-sync Workload type")}},{name:"customer grants POST a native Customer body and egress a typed Customer membership",fn:async()=>{let Ne=It(),Le=ks();await Le.customerStore.upsert({id:"cust-delta",externalId:"cust-delta",userName:"cust-delta",name:"Delta Customer",email:"delta-customer@example.test",loyaltyTier:"gold"}),await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...Le,mapCustomerToScim:(We)=>({...Fl(We),email:We.email,loyaltyTier:We.loyaltyTier}),fetchImpl:Ne.fetchImpl}).onGrant({subject:"cust-delta",subjectType:"customer",targetType:"role",target:{id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0,assignableSubjectTypes:["customer"]}});let Ve=Ne.calls.find((We)=>We.method==="POST"&&We.path.endsWith("/Customers"));e(Ve!==void 0,"customer subjects POST a native Customer destination body"),e(Ve?.body?.schemas?.includes(Ql)===!0,"the Customer body declares the ionite Customer schema URN"),e(Ve?.body?.email==="delta-customer@example.test","the AppDev customer projection may widen IAM delta with PII"),e(Ve?.body?.loyaltyTier==="gold","the AppDev customer projection reaches IAM delta"),e(!Ne.calls.some((We)=>We.method==="POST"&&We.path.endsWith("/Users")),"customer subjects do not fabricate a destination User body");let Ge=Ne.calls.find((We)=>We.method==="PATCH")?.body?.Operations?.[0]?.value?.[0];e(Ge?.type==="Customer","the membership PATCH carries type Customer")}},{name:"switching IAM destinations self-heals the same row receipt without destination-qualified mappings",fn:async()=>{let Ne=It("https://iam-a.example.test/scim/v2"),Le=It("https://iam-b.example.test/scim/v2");Ne.seedGroup("g-eng","g_at_a"),Le.seedGroup("g-eng","g_at_b");let ze=nr();await hs(Ne,ze).client.resolveProviderId("group","g-eng"),e(ze.get("group","g-eng")?.providerId==="g_at_a","the row initially carries destination A");let De=await hs(Le,ze).client.upsertResource("group","g-eng",{displayName:"Engineering"});e(De==="pushed","the first write after a destination switch self-heals and succeeds"),e(Le.calls.some((Ve)=>Ve.method==="PUT"&&Ve.path.endsWith("/Groups/g_at_a")),"the new client first observes the stale row receipt"),e(Le.calls.some((Ve)=>Ve.method==="GET"&&Ve.path.endsWith("/Groups")&&Ve.filter?.includes('externalId eq "g-eng"')),"a stale destination receipt is refreshed strictly by externalId"),e(Le.calls.some((Ve)=>Ve.method==="PUT"&&Ve.path.endsWith("/Groups/g_at_b")),"the write retries once against destination B provider id"),e(ze.get("group","g-eng")?.providerId==="g_at_b","the same row now carries destination B")}},{name:"receipt-only writes do not recursively egress, while riskLevel and deprecated updates do",fn:async()=>{let Ne=It(),Le=new Oi,ze=new qi,De=globalThis.fetch;globalThis.fetch=async(Je,Ge)=>{let We=typeof Je==="string"?Je:Je instanceof URL?Je.toString():Je.url;if(We==="https://idp.example.test/token")return new Response(JSON.stringify({access_token:"test-token",token_type:"Bearer",expires_in:3600}),{status:200,headers:Wl});return Ne.fetchImpl(We,Ge)};let Ve=tu(ou({id:"ecv-delta-row-receipts",workload:{outgoing:{iam:{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1}}},iam:{url:Ne.baseUrl,deltaAudience:"iam",users:{delta:"patch"}}}),{validators:au(),iam:{userStore:ze},stores:{catalogStore:Le}});try{await Ve.ready(1000),e(Ve.iam?.delta?.isReady()===!0,"managed delta is ready with both receipt row stores");let Je={id:"reviewers",name:"Reviewers",description:"Review access",requestable:!0,source:"app",assignableSubjectTypes:["user"],first:1,second:2};await Le.groups.set(Je).ready();for(let Qe=0;Qe<100;Qe++){if((await Le.groups.get("reviewers"))?.providerId)break;await new Promise((st)=>setTimeout(st,5))}let Ge=await Le.groups.get("reviewers");e(Ge?.providerId?.startsWith("g_")===!0,"destination receipt is written onto the Group row"),e(Ge?.requestUrl?.includes("/request-access/")===!0,"destination request URL is written onto the Group row");let We=Ne.countCalls((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups"));e(We===1,"the receipt-only catalog set is suppressed instead of recursively issuing another entitlement upsert");let He=Ne.calls.find((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups"))?.body?.[Zs];e(He?.attributes?.first===1&&He.attributes.second===2,"the entitlement delta body carries the same flat custom attributes as SCIM pull");let Xe={...Ge,second:2,first:1,requestUrl:`${Ge?.requestUrl}?verified=true`};await Le.groups.set(Xe).ready(),await new Promise((Qe)=>setTimeout(Qe,20)),e(Ne.countCalls((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups"))===1,"a receipt-only update is suppressed even when object keys are reconstructed in a different order");let Ye={...Xe,riskLevel:"elevated"};await Le.groups.set(Ye).ready();for(let Qe=0;Qe<100;Qe++){if(Ne.countCalls((st)=>(st.method==="POST"||st.method==="PUT")&&st.path.includes("/Groups"))>=2)break;await new Promise((st)=>setTimeout(st,5))}let tt=Ne.calls.filter((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups")).at(-1)?.body?.[Zs];e(Ne.countCalls((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups"))===2,"riskLevel is a substantive row update and egresses exactly once"),e(tt?.riskLevel==="elevated","delta projects the updated riskLevel"),await Le.groups.set({...Ye}).ready(),await new Promise((Qe)=>setTimeout(Qe,20)),e(Ne.countCalls((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups"))===2,"a value-identical riskLevel write does not egress"),await Le.groups.set({...Ye,deprecated:!0}).ready();for(let Qe=0;Qe<100;Qe++){if(Ne.countCalls((st)=>(st.method==="POST"||st.method==="PUT")&&st.path.includes("/Groups"))>=3)break;await new Promise((st)=>setTimeout(st,5))}e(Ne.countCalls((Qe)=>(Qe.method==="POST"||Qe.method==="PUT")&&Qe.path.includes("/Groups"))===3,"deprecated:true is a substantive row update and egresses exactly once")}finally{Ve.close?.(),globalThis.fetch=De}}},{name:"reconcile pushes definitions and users before their access roots",fn:async()=>{let Ne=It(),Le=bs(cs),ze=ks(),De=Le.update({...Zt,url:Ne.baseUrl},{workload:es,...ze,fetchImpl:Ne.fetchImpl}),Ve={id:"alice",externalId:"u-alice",userName:"alice@example.test",name:"Alice",email:"alice@example.test"},Je={id:"eng",name:"Engineering",description:"Engineering access",requestable:!0,assignableSubjectTypes:["user"]};await ze.userStore.upsert(Ve),await ze.catalogStore.groups.set(Je).ready();let Ge=await De.reconcile({groups:[Je],users:[Ve],access:[{subject:Ve,subjectType:"user",targetType:"group",target:Je}]});e(Ge.pushed===3,"definition, user, and access root are all counted as pushed");let We=await ze.catalogStore.groups.get("eng"),Ke=await ze.userStore.get("alice");e(We?.providerId?.startsWith("g_")===!0,"first Group resolution persists providerId on row"),e(We?.requestUrl?.includes("/request-access/")===!0,"first Group resolution persists IAM requestUrl on row"),e(Ke?.providerId?.startsWith("u_")===!0,"first User resolution persists providerId on row");let Fe=Ne.calls.findIndex((Qe)=>Qe.method==="POST"&&Qe.path.endsWith("/Groups")),He=Ne.calls.findIndex((Qe)=>Qe.method==="POST"&&Qe.path.endsWith("/Users")),Xe=Ne.calls.findIndex((Qe)=>Qe.method==="PATCH");e(Fe>=0&&Fe<Xe,"Group definition is pushed before membership"),e(He>=0&&He<Xe,"User definition is pushed before membership");let Ye=await De.getDestinationRef("group","eng");e(Ye?.providerId.startsWith("g_")===!0,"the emitter exposes the captured provider id"),e(Ye?.requestUrl?.includes("/request-access/")===!0,"the emitter exposes the IAM-owned request URL without another network lookup");let Ze=Ne.countCalls((Qe)=>Qe.method==="GET"),tt=await De.reconcile({groups:[Je],users:[Ve]});e(tt.errors.length===0,"later full-body pushes succeed using persisted row receipts"),e(Ne.countCalls((Qe)=>Qe.method==="GET")===Ze,"later Group and User pushes reuse row receipts without another externalId lookup")}},{name:"readiness: patch needs url + workload + UserStore + AccessCatalogStore",fn:async()=>{let Ne=bs(cs),Le=ks(),ze=Ne.update({...Zt},{workload:es,...Le});e(!ze.isReady(),"'patch' without a destination url is not ready");let De=await ze.reconcile({});e(De.errors[0]==="iam delta not ready: destination URL is not configured","an attempted operation reports the failed readiness precondition");let Ve=Ne.update({...Zt,url:"https://iam.example.test/scim/v2"},Le);e(!Ve.isReady(),"'patch' without a workload identity is not ready");let Je=Ne.update({...Zt,url:"https://iam.example.test/scim/v2"},{workload:es,catalogStore:Le.catalogStore});e(!Je.isReady(),"'patch' without a UserStore cannot persist User receipts");let Ge=Ne.update({...Zt,url:"https://iam.example.test/scim/v2"},{workload:es,userStore:Le.userStore});e(!Ge.isReady(),"'patch' without an AccessCatalogStore cannot persist Group receipts");let We=Ne.update({...Zt,url:"https://iam.example.test/scim/v2"},{workload:es,...Le});e(We.isReady(),"'patch' with url + workload + both receipt row stores is ready"),await We.ready(1000);let Ke=Ui(void 0,cs,{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1});e(typeof Ke?.isReady==="function"&&typeof Ke.ready==="function","the raw configured workload runtime exposes readiness"),e(Ke.isReady(),"the raw configured workload runtime is ready"),await Ke.ready(1000);let Fe=Ne.update({...Zt,url:"https://iam.example.test/scim/v2"},{workload:Ke,...Le});e(Fe.isReady(),"IAM delta accepts the workload runtime built during config apply");let He=Ui(void 0,cs,{outgoing:{iam:{idpTokenUrl:"https://idp.example.test/token",clientId:"iam-client",clientSecret:"test-secret",autoRefresh:!1}}});e(typeof He?.isReady==="function"&&typeof He.ready==="function"&&"iam"in He,"the aggregate workload runtime exposes module and named-client readiness"),e(He.isReady(),"the aggregate workload runtime is ready");let Xe=Ne.update({...Zt,url:"https://iam.example.test/scim/v2",audience:"iam"},{workload:He,...Le});e(Xe.isReady(),"IAM delta accepts the aggregate workload runtime and named outgoing client used by vault tenants");let Ye=Ne.update({url:"https://iam.example.test/scim/v2"},{workload:es,...Le});e(!Ye.isReady(),"removing delta mode de-configures the stable handle");let Ze=!1;try{await Ye.ready(1)}catch{Ze=!0}e(Ze,"ready() re-arms while IAM delta is de-configured"),Ne.update({...Zt,url:"https://iam.example.test/scim/v2"},{workload:es,...Le}),await Ye.ready(1000)}},{name:"emitter skips an access root whose subject or target cannot be resolved",fn:async()=>{let Le=bs(cs).update({...Zt,url:"https://iam.example.test/scim/v2"},{workload:es,...ks()});e(Le.isReady(),"emitter is ready for the reconcile path");async function*ze(){yield{subject:"alice",subjectType:"user",targetType:"group",target:"eng",status:"active"}}let De=await Le.reconcile({access:ze()});e(De.pushed===0,"an unresolvable subject/target is not pushed"),e(De.skipped===1,"the unresolved root is skipped"),e(De.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 Ne=It();Ne.seedUser("u-alice","u_1"),Ne.seedGroup("g-eng","g_1"),Ne.failGroupPatchWith(500);let{client:Le}=hs(Ne),ze=await Le.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add"});e(ze==="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 Ne=It();Ne.failLookupWith(500);let{client:Le}=hs(Ne),ze=await Le.patchGroupMembership({groupExternalId:"g-eng",userExternalId:"u-alice",op:"add",groupCreate:{displayName:"Eng"},userCreate:{userName:"alice"}});e(ze==="error",'a failed lookup is reported as an error, not "skipped" or "pushed"'),e(Ne.countCalls((De)=>De.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 Ne=It();Ne.seedUser("u-alice","u_1"),Ne.seedGroup("eng","g_1"),Ne.failGroupPatchWith(503);let ze=bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...ks(),fetchImpl:Ne.fetchImpl});async function*De(){yield{subject:{id:"alice",externalId:"u-alice",userName:"alice"},subjectType:"user",targetType:"group",target:{id:"eng",displayName:"Eng",requestable:!0},status:"active"}}let Ve=await ze.reconcile({access:De()});e(Ve.pushed===0,"a rejected destination push is not counted as pushed"),e(Ve.errors.length>0,"a rejected destination PATCH is recorded as a reconcile error")}},{name:"per-kind-off skips principal and membership egress for that subject kind",fn:async()=>{let Ne=It(),ze=bs(cs).update({users:"patch",url:Ne.baseUrl},{workload:es,...ks(),fetchImpl:Ne.fetchImpl});await ze.onGrant({subject:"cust-off",subjectType:"customer",targetType:"role",target:{id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0,assignableSubjectTypes:["customer"]}}),e(!Ne.calls.some((De)=>De.path.includes("/Customers")),"customer delta off does not POST a Customer body"),e(!Ne.calls.some((De)=>De.method==="PATCH"),"customer delta off does not PATCH group membership"),await ze.onEntitlementUpsert({id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0,assignableSubjectTypes:["user"]},"role"),e(Ne.calls.some((De)=>De.method==="POST"&&De.path.endsWith("/Groups")),"the Groups channel still egresses when any kind delta is patch")}},{name:"customer receipts persist providerId on the customer row",fn:async()=>{let Ne=It(),Le=ks();await Le.customerStore.upsert({id:"cust-receipt",userName:"cust-receipt",name:"Receipt Customer",email:"",externalId:"cust-receipt",displayName:"Receipt Customer",active:!0}),await bs(cs).update({...Zt,url:Ne.baseUrl},{workload:es,...Le,fetchImpl:Ne.fetchImpl}).onGrant({subject:"cust-receipt",subjectType:"customer",targetType:"role",target:{id:"reviewer",name:"Reviewer",description:"Reviewer role",requestable:!0,assignableSubjectTypes:["customer"]}});let Ve=await Le.customerStore.get("cust-receipt");e(Ve?.providerId?.startsWith("c_")===!0,"first Customer resolution persists providerId on row")}}]}}async function he(){let Be=performance.now(),Ne=[],Le=[...y().tests];for(let Je of Le)Ne.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Ne.filter((Je)=>!Je.passed).length,Ve=Ne.filter((Je)=>Je.details?.skipped).length;return{suite:"IAM Delta",passed:De===0,tests:Ne,duration:ze,summary:{total:Ne.length,passed:Ne.length-De-Ve,failed:De,skipped:Ve}}}import{ACCEPT_SCIM as ru,CONTENT_TYPE_SCIM as iu,ION_SCIM_CUSTOMER_SCHEMA as or,ION_SCIM_WORKLOAD_SCHEMA as Kn}from"@ionite/core";import{InMemoryCustomerStore as cn,InMemoryWorkloadIdentityStore as dn,iam as cu,silentLogger as du}from"@ionite/server";import{createValidators as lu}from"@ionite/valibot";var uu=lu(),pu="urn:ietf:params:scim:api:messages:2.0:ListResponse",mu={isReady:()=>!0,ready:async()=>{return},validateToken:async()=>({valid:!0}),getToken:async()=>"test-token"},hu={oaa:{isReady:()=>!1}};function Kt(){return{Authorization:"Bearer test-token",...ru,...iu}}function Qs(Be){let Ne=cu(uu.iam,du,mu,hu,void 0,void 0,void 0,void 0,void 0,{usersUrl:"/api/iam/Users",groupsUrl:"/api/iam/Groups",workloadsUrl:"/api/iam/Workloads",customersUrl:"/api/iam/Customers",...Be});return e(Ne!==void 0,"IAM handler is constructed"),Ne}function w(){return{tests:[{name:"ResourceTypes and Schemas advertise Workload and Customer when stores are wired",fn:async()=>{let Ne=Qs({workloadIdentityStore:new dn,customerStore:new cn}),Le=await Ne.handler(new Request("https://ecv.test/api/iam/ResourceTypes",{headers:Kt()}));e(Le.status===200,`ResourceTypes returned ${Le.status}`);let ze=await Le.json();e(ze.Resources?.some((Je)=>Je.id==="Workload")===!0,"ResourceTypes includes Workload"),e(ze.Resources?.some((Je)=>Je.id==="Customer")===!0,"ResourceTypes includes Customer");let Ve=await(await Ne.handler(new Request("https://ecv.test/api/iam/Schemas",{headers:Kt()}))).json();e(Ve.Resources?.some((Je)=>Je.id===Kn)===!0,"Schemas includes the Workload URN"),e(Ve.Resources?.some((Je)=>Je.id===or)===!0,"Schemas includes the Customer URN")}},{name:"Workloads CRUD, filters, attributes, and pagination",fn:async()=>{let Ne=new dn,Le=Qs({workloadIdentityStore:Ne}),ze=await Le.handler(new Request("https://ecv.test/api/iam/Workloads",{method:"POST",headers:Kt(),body:JSON.stringify({schemas:[Kn],userName:"billing-worker",displayName:"Billing worker",externalId:"w-billing",active:!0})}));e(ze.status===201,`Workload create returned ${ze.status}: ${await ze.clone().text()}`);let De=await ze.json();e(De.userName==="billing-worker","created Workload carries userName from clientId");let Je=await(await Le.handler(new Request("https://ecv.test/api/iam/Workloads?filter=userName%20eq%20%22billing-worker%22",{headers:Kt()}))).json();e(Je.schemas?.includes(pu)===!0,"list is a SCIM ListResponse"),e(Je.totalResults===1,"userName eq filter matches the created Workload");let Ge=await Le.handler(new Request("https://ecv.test/api/iam/Workloads?filter=externalId%20eq%20%22w-billing%22",{headers:Kt()}));e((await Ge.json()).totalResults===1,"externalId eq filter matches the created Workload");let We=await Le.handler(new Request("https://ecv.test/api/iam/Workloads",{method:"POST",headers:Kt(),body:JSON.stringify({schemas:[Kn],userName:"other-worker",displayName:"Other",externalId:"w-other",active:!0})}));e(We.status===201,`second Workload create returned ${We.status}`);let Fe=await(await Le.handler(new Request("https://ecv.test/api/iam/Workloads?startIndex=1&count=1",{headers:Kt()}))).json();e(Fe.totalResults===2,"pagination reports both workloads"),e(Fe.itemsPerPage===1,"count=1 limits the page");let Ye=(await(await Le.handler(new Request("https://ecv.test/api/iam/Workloads?attributes=id,displayName",{headers:Kt()}))).json()).Resources?.[0];e(Ye?.displayName!==void 0,"attributes= keeps displayName"),e(Ye?.userName===void 0,"attributes= drops userName")}},{name:"Customers CRUD, lastModified filter, and excludedAttributes",fn:async()=>{let Ne=new cn,Le=Qs({customerStore:Ne}),ze=await Le.handler(new Request("https://ecv.test/api/iam/Customers",{method:"POST",headers:Kt(),body:JSON.stringify({schemas:[or],userName:"ada",displayName:"Ada Lovelace",externalId:"c-ada",active:!0,emails:[{value:"ada@example.test",primary:!0}]})}));e(ze.status===201,`Customer create returned ${ze.status}: ${await ze.clone().text()}`);let De=new Date(Date.now()-60000).toISOString(),Ve=await Le.handler(new Request(`https://ecv.test/api/iam/Customers?filter=meta.lastModified%20gt%20%22${De}%22`,{headers:Kt()}));e((await Ve.json()).totalResults===1,"meta.lastModified gt returns the newly written customer");let Ge=await(await Le.handler(new Request("https://ecv.test/api/iam/Customers?excludedAttributes=emails",{headers:Kt()}))).json();e(Ge.Resources?.[0]?.emails===void 0,"excludedAttributes drops emails"),e(Ge.Resources?.[0]?.displayName!==void 0,"excludedAttributes keeps displayName")}},{name:"customer and workload pull surfaces use AppDev-owned projections",fn:async()=>{let Ne=new cn,Le=new dn;await Ne.upsert({id:"customer-projection",externalId:"customer-projection",userName:"customer-projection",name:"Projected Customer",email:"projected@example.test",loyaltyTier:"gold"}),await Le.upsert({id:"workload-projection",externalId:"workload-projection",displayName:"Projected Workload",ownerTeam:"payments"});let ze=Qs({customerStore:Ne,workloadIdentityStore:Le,inboundCustomers:{mapBaseCustomerToScim:(We)=>({schemas:[or],id:We.id,externalId:We.externalId,displayName:We.displayName??We.name,active:We.active??!0,loyaltyTier:We.loyaltyTier})},inboundWorkloads:{mapBaseWorkloadToScim:(We)=>({schemas:[Kn],id:We.id,externalId:We.externalId,userName:We.clientId??We.id,displayName:We.displayName,active:We.active??!0,ownerTeam:We.ownerTeam})}}),Ve=await(await ze.handler(new Request("https://ecv.test/api/iam/Customers/customer-projection",{headers:Kt()}))).json();e(Ve.loyaltyTier==="gold","customer pull uses mapBaseCustomerToScim");let Ge=await(await ze.handler(new Request("https://ecv.test/api/iam/Workloads/workload-projection",{headers:Kt()}))).json();e(Ge.ownerTeam==="payments","workload pull uses mapBaseWorkloadToScim")}},{name:"disabled principal kinds 404 and enabled-without-store 501",fn:async()=>{let Ne=Qs({workloads:{enabled:!1},customers:{enabled:!1},workloadIdentityStore:new dn,customerStore:new cn}),Le=await Ne.handler(new Request("https://ecv.test/api/iam/Workloads",{headers:Kt()})),ze=await Ne.handler(new Request("https://ecv.test/api/iam/Customers",{headers:Kt()}));e(Le.status===404,`disabled Workloads returned ${Le.status}`),e(ze.status===404,`disabled Customers returned ${ze.status}`);let De=Qs({workloads:{enabled:!0},customers:{enabled:!0}}),Ve=await De.handler(new Request("https://ecv.test/api/iam/Workloads",{headers:Kt()})),Je=await De.handler(new Request("https://ecv.test/api/iam/Customers",{headers:Kt()}));e(Ve.status===501,`Workloads without a store returned ${Ve.status}`),e(Je.status===501,`Customers without a store returned ${Je.status}`)}},{name:"AccessRequests validates operations, reuses pending tuples, and 404s when unwired",fn:async()=>{let Ne=[],Le=Qs({accessRequests:{create:async(Xe)=>{let Ye=Ne.find((tt)=>tt.subject===Xe.subject&&tt.subjectType===Xe.subjectType&&tt.target===Xe.target&&tt.operation===Xe.operation);if(Ye)return{request:{id:String(Ye.id),status:"pending",operation:Xe.operation,trackingUrl:String(Ye.trackingUrl)},created:!1};let Ze={id:`req_${Ne.length+1}`,status:"pending",trackingUrl:`https://iam.example.test/requests/req_${Ne.length+1}`,subject:Xe.subject,subjectType:Xe.subjectType,target:Xe.target,operation:Xe.operation};return Ne.push(Ze),{request:{id:Ze.id,status:"pending",operation:Xe.operation,trackingUrl:Ze.trackingUrl},created:!0}}}}),ze=await Le.handler(new Request("https://ecv.test/api/iam/AccessRequests",{method:"POST",headers:Kt(),body:JSON.stringify({subject:"cust-1",subjectType:"customer",targetType:"group",target:"beta_access",operation:"grant"})}));e(ze.status===201,`AccessRequests create returned ${ze.status}`);let De=await ze.json(),Ve=await Le.handler(new Request("https://ecv.test/api/iam/AccessRequests",{method:"POST",headers:Kt(),body:JSON.stringify({subject:"cust-1",subjectType:"customer",targetType:"group",target:"beta_access",operation:"grant"})}));e(Ve.status===200,`AccessRequests reuse returned ${Ve.status}`);let Je=await Ve.json();e(De.id===Je.id,"pending AccessRequests are idempotent at the destination hook"),e(Je.operation==="grant","the operation is echoed in the response");let Ge=await Le.handler(new Request("https://ecv.test/api/iam/AccessRequests",{method:"POST",headers:Kt(),body:JSON.stringify({subject:"cust-1",subjectType:"customer",targetType:"group",target:"beta_access",operation:"revoke"})}));e(Ge.status===201,`distinct revoke AccessRequest returned ${Ge.status}`);let We=await Ge.json();e(We.id!==De.id,"grant and revoke are distinct pending tuples"),e(We.operation==="revoke","the revoke operation is echoed");let Ke=await Le.handler(new Request("https://ecv.test/api/iam/AccessRequests",{method:"POST",headers:Kt(),body:JSON.stringify({subject:"cust-1",subjectType:"customer",targetType:"group",target:"beta_access"})}));e(Ke.status===400,`AccessRequests without operation returned ${Ke.status}`);let He=await Qs({}).handler(new Request("https://ecv.test/api/iam/AccessRequests",{method:"POST",headers:Kt(),body:JSON.stringify({subject:"cust-1",subjectType:"customer",targetType:"group",target:"beta_access",operation:"grant"})}));e(He.status===404,`unwired AccessRequests returned ${He.status}`)}},{name:"principal writes require a bearer token",fn:async()=>{let Le=await Qs({workloadIdentityStore:new dn,customerStore:new cn}).handler(new Request("https://ecv.test/api/iam/Workloads"));e(Le.status===401,`unauthenticated Workloads returned ${Le.status}`)}}]}}async function ge(){let Be=performance.now(),Ne=[];for(let Ve of w().tests)Ne.push(await s(Ve.name,async()=>{await Ve.fn();return}));let Le=performance.now()-Be,ze=Ne.filter((Ve)=>!Ve.passed).length,De=Ne.filter((Ve)=>Ve.details?.skipped).length;return{suite:"IAM Principals",passed:ze===0,tests:Ne,duration:Le,summary:{total:Ne.length,passed:Ne.length-ze-De,failed:ze,skipped:De}}}import{ionite as ds}from"@ionite/server";import{createValidators as ls}from"@ionite/valibot";var wa=(Be)=>new Promise((Ne)=>setTimeout(Ne,Be));async function Ts(Be,Ne=1500){let Le=Date.now()+Ne;while(Date.now()<Le){if(Be())return!0;await wa(10)}return Be()}function us(Be,Ne,Le={}){let ze=Be,De,Ve=0,Je=new Map(Object.entries(Ne)),Ge=new Map,We=new Map,Ke=new Map,Fe=new Map,He=[],Xe=[],Ye=[],Ze=new Set,tt=(et)=>{let nt=Je.get(et);if(nt===void 0)throw Error(`Missing test secret ${et}`);return{data:structuredClone(nt),metadata:{path:et,version:Ve,created:new Date(0)}}},Qe=(et)=>{for(let nt of Ge.get(et)??[])nt(tt(et))};return{secret:{type:"dev",isReady:()=>!0,ready:async()=>{},isAvailable:()=>!0,get:()=>{throw Error("not implemented")},getFullSecret:async(et)=>(Ke.set(et,(Ke.get(et)??0)+1),tt(et)),getSecret:async(et)=>(Ke.set(et,(Ke.get(et)??0)+1),tt(et).data),putSecret:async()=>{throw Error("not implemented")},deleteSecret:async()=>{throw Error("not implemented")},listPaths:async()=>[],exists:async(et)=>Je.has(et),requestRotate:async()=>{},requestRevoke:async()=>{},parkKmsKey:async(et,nt)=>(He.push({path:et,request:{...nt}}),{parked:!0,disposition:nt.status===403||nt.status===401?"suspect":"cooldown"}),addKmsKeys:async(et,nt)=>(Xe.push({path:et,request:{keys:nt.keys.map((dt)=>({...dt}))}}),{keys:nt.keys.map((dt,pt)=>({id:dt.id??`issued-${pt+1}`}))}),revokeKmsKey:async(et,nt)=>(Ye.push({path:et,request:{...nt}}),{revoked:!0}),...Le.events?{subscribeEvents:(et)=>(Ze.add(et),()=>Ze.delete(et))}:{},getMetadata:async()=>({}),subscribe:async(et,nt,dt)=>{We.set(et,(We.get(et)??0)+1),Fe.set(et,{...dt?.kmsSlots===void 0?{}:{kmsSlots:dt.kmsSlots}});let pt=Ge.get(et)??new Set;if(pt.add(nt),Ge.set(et,pt),!Le.delayInitialDelivery)nt(tt(et));return()=>pt.delete(nt)}},async load(){return ze},async subscribe(et){return De=et,()=>{De=void 0}},pushConfig(et){ze=et,De?.(et)},pushPool(et,nt){Ve+=1,Je.set(et,structuredClone(nt)),Qe(et)},deliverPool(et){Qe(et)},subscriptionCount(et){return We.get(et)??0},fetchCount(et){return Ke.get(et)??0},lastSubscribeOptions(et){return Fe.get(et)},parkCalls(){return He},issueCalls(){return Xe},revokeCalls(){return Ye},emitEvent(et){for(let nt of Ze)nt(et)}}}function ps(Be,Ne,Le){return{kind:"kms",slots:[Be],keys:[{id:Be,state:"active",value:Ne,notBefore:new Date(0).toISOString()}],...Le}}async function Ns(Be,Ne){let Le=!1;try{await Be}catch{Le=!0}e(Le,Ne)}function v(){return{tests:[{name:"kms selects the active key and preserves graceful-rotation overlap until removal or revocation",fn:async()=>{let Ne=us({id:"kms-rotation",kms:{rotation:{path:"/kms/rotation"}}},{["/kms/rotation"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("rotation");e(ze?.isReady()===!0,"Expected configured KMS pool to become ready"),t(await ze.value(),"token-1","Expected value() to select the declared active key"),Ne.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()}]}),t(await ze.value(),"token-2","Expected rotation to select the new active key"),e(ze.accepts("token-1"),"Expected the deprecated overlap key to remain accepted"),e(ze.accepts("token-2"),"Expected the new active key to be accepted"),Ne.pushPool("/kms/rotation",{kind:"kms",slots:["k2"],keys:[{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),e(!ze.accepts("token-1"),"Expected a removed overlap key to stop being accepted immediately"),Ne.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()}]}),e(ze.accepts("token-2"),"Expected the second graceful overlap key to remain accepted"),Ne.pushPool("/kms/rotation",{kind:"kms",slots:["k3"],keys:[{id:"k2",state:"revoked"},{id:"k3",state:"active",value:"token-3",notBefore:new Date(0).toISOString()}]}),e(!ze.accepts("token-2"),"Expected a revoked overlap key to stop being accepted immediately"),t(await ze.value(),"token-3","Expected revocation not to disturb the current active key")}finally{Le.close()}}},{name:"kms hard replacement drops the old key immediately",fn:async()=>{let Ne=us({id:"kms-hard-replacement",kms:{hard:{path:"/kms/hard-replacement"}}},{["/kms/hard-replacement"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("hard");e(ze?.isReady()===!0,"Expected the initial hard-replacement pool grant"),e(ze.accepts("token-1"),"Expected the initial key to be accepted"),Ne.pushPool("/kms/hard-replacement",ps("k2","token-2")),t(await ze.value(),"token-2","Expected hard replacement to select the replacement key"),e(!ze.accepts("token-1"),"Expected hard replacement to drop the old key immediately"),e(ze.accepts("token-2"),"Expected hard replacement to accept the replacement key")}finally{Le.close()}}},{name:"kms empty grants re-arm value waiting and a later grant resolves the pending call",fn:async()=>{let Ne=us({id:"kms-empty-grant",kms:{empty:{path:"/kms/empty-grant"}}},{["/kms/empty-grant"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("empty");e(ze?.isReady()===!0,"Expected the initial pool grant"),Ne.pushPool("/kms/empty-grant",{kind:"kms",keys:[]}),e(await Ts(()=>!ze.isReady()),"Expected an empty grant to make the pool not ready"),e(!ze.accepts("token-1"),"Expected an empty grant to revoke the cached key"),await Ns(ze.value(30),"Expected value(timeout) to reject when the empty grant is not replaced");let De=!1,Ve=ze.value().then((Je)=>(De=!0,Je));await wa(20),e(!De,"Expected value() to remain pending while the grant is empty"),Ne.pushPool("/kms/empty-grant",ps("k2","token-2")),t(await Ve,"token-2","Expected a later grant to resolve the already-pending value()"),e(ze.isReady(),"Expected the pool to become ready after the later grant")}finally{Le.close()}}},{name:"kms concurrent first value calls wait for initial delivery through one source subscription",fn:async()=>{let Ne=us({id:"kms-delayed-initial",kms:{delayed:{path:"/kms/delayed-initial"}}},{["/kms/delayed-initial"]:ps("k1","token-1")},{delayInitialDelivery:!0}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("delayed");e(ze!==void 0,"Expected the delayed pool handle"),e(!ze.isReady(),"Expected the pool to wait for its initial grant delivery");let De=Array.from({length:8},()=>ze.value());e(De.every((Je)=>Je===De[0]),"Expected all concurrent value() calls to share the same pending waiter");let Ve=!1;De[0]?.then(()=>{Ve=!0}),await wa(20),e(!Ve,"Expected the first value() to wait before initial delivery"),t(Ne.subscriptionCount("/kms/delayed-initial"),1,"Expected all concurrent callers to share one source subscription"),t(Ne.fetchCount("/kms/delayed-initial"),0,"Expected concurrent value() calls not to issue duplicate fetches"),Ne.deliverPool("/kms/delayed-initial"),t((await Promise.all(De)).join(","),Array.from({length:8},()=>"token-1").join(","),"Expected the initial delivery to resolve every concurrent caller with the same grant"),t(Ne.subscriptionCount("/kms/delayed-initial"),1,"Expected initial delivery not to create a new subscription"),t(Ne.fetchCount("/kms/delayed-initial"),0,"Expected initial delivery to avoid one-off source fetches")}finally{Le.close()}}},{name:"kms cached repeated value calls create no additional source calls",fn:async()=>{let Ne=us({id:"kms-cached",kms:{cached:{path:"/kms/cached"}}},{["/kms/cached"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("cached");e(ze?.isReady()===!0,"Expected the cached pool grant");let De=Ne.subscriptionCount("/kms/cached"),Ve=Ne.fetchCount("/kms/cached"),Je=await Promise.all(Array.from({length:20},()=>ze.value()));e(Je.every((Ge)=>Ge==="token-1"),"Expected every cached call to return the active key"),t(Ne.subscriptionCount("/kms/cached"),De,"Expected cached value() calls not to add subscriptions"),t(Ne.fetchCount("/kms/cached"),Ve,"Expected cached value() calls not to fetch")}finally{Le.close()}}},{name:"kms value waits for a future notBefore boundary",fn:async()=>{let Ne=us({id:"kms-not-before",kms:{scheduled:{path:"/kms/not-before"}}},{["/kms/not-before"]:ps("initial","initial-token")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("scheduled");e(ze!==void 0,"Expected the scheduled pool handle"),e(ze.isReady(),"Expected the initial pool grant");let De=new Date(Date.now()+150).toISOString();Ne.pushPool("/kms/not-before",ps("future","future-token",{keys:[{id:"future",state:"active",value:"future-token",notBefore:De}]})),e(!ze.isReady(),"Expected the future notBefore key to remain unavailable initially");let Ve=!1,Je=ze.value(1000).then((Ge)=>(Ve=!0,Ge));await wa(30),e(!Ve,"Expected value() to wait before the notBefore boundary"),t(await Je,"future-token","Expected value() to resolve when notBefore becomes current"),e(ze.isReady(),"Expected the pool to become ready at the notBefore boundary")}finally{Le.close()}}},{name:"kms de-configuration rejects a pending value and re-arms the stable pool handle",fn:async()=>{let Ne=us({id:"kms-rearm",kms:{rearm:{path:"/kms/rearm"}}},{["/kms/rearm"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("rearm");e(ze?.isReady()===!0,"Expected the initial pool grant"),Ne.pushPool("/kms/rearm",{kind:"kms",keys:[]}),e(await Ts(()=>!ze.isReady()),"Expected the empty grant to re-arm value waiting");let De=ze.value().then(()=>"resolved",()=>"rejected");Ne.pushConfig({id:"kms-rearm"}),e(await Ts(()=>!ze.isReady()),"Expected the pool to remain not ready after removal"),t(await Promise.race([De,wa(300).then(()=>"timed-out")]),"rejected","Expected de-configuration to reject the pending value() call"),await Ns(ze.ready(30),"Expected the re-armed pool ready(timeout) to reject while de-configured");let Ve=!1,Je=ze.ready().then(()=>{Ve=!0});await wa(20),e(!Ve,"Expected the re-armed ready() waiter to stay pending while de-configured"),Ne.pushPool("/kms/rearm",ps("k2","token-2")),Ne.pushConfig({id:"kms-rearm",kms:{rearm:{path:"/kms/rearm"}}}),e(await Ts(()=>ze.isReady()),"Expected the same stable pool handle to recover after re-configuration"),await Je,e(Ve,"Expected the re-armed ready() waiter to resolve after re-configuration"),t(await ze.value(),"token-2","Expected the restored handle to expose the later grant")}finally{Le.close()}}},{name:"kms wrong document kind and shape fail closed until value timeout or a valid grant",fn:async()=>{let Ne=us({id:"kms-invalid",kms:{invalid:{path:"/kms/invalid"}}},{["/kms/invalid"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("invalid");e(ze?.isReady()===!0,"Expected the initial valid grant"),Ne.pushPool("/kms/invalid",{kind:"token",slots:["wrong-kind"],keys:[{id:"wrong-kind",state:"active",value:"wrong-token"}]}),e(await Ts(()=>!ze.isReady()),"Expected a wrong document kind to fail closed"),e(!ze.accepts("token-1"),"Expected invalid delivery to clear previously accepted keys");let De=performance.now();await Ns(ze.value(40),"Expected value(timeout) to reject while a wrong-kind document remains current"),e(performance.now()-De>=20,"Expected wrong-kind value() to wait for its timeout instead of rejecting immediately"),Ne.pushPool("/kms/invalid",{kind:"kms",slots:["missing"],keys:[]}),e(await Ts(()=>!ze.isReady()),"Expected an invalid KMS document shape to fail closed");let Ve=performance.now();await Ns(ze.value(40),"Expected value(timeout) to reject while an invalid-shape document remains current"),e(performance.now()-Ve>=20,"Expected invalid-shape value() to wait for its timeout instead of rejecting immediately"),Ne.pushPool("/kms/invalid",ps("k2","token-2")),t(await ze.value(100),"token-2","Expected a later valid grant to recover the failed-closed pool")}finally{Le.close()}}},{name:"otel apiKey and apiKeyPool are rejected when configured together",fn:async()=>{let Ne=us({id:"kms-conflict",kms:{conflict:{path:"/kms/conflict"}},otel:{name:"kms-conflict",apiKey:"static",apiKeyPool:"conflict",traces:!1,metrics:!1,logs:!1}},{["/kms/conflict"]:ps("k1","managed")}),Le=ds(Ne,{validators:ls()});try{await Ns(Le.ready(500),"Expected mutually exclusive OTel credential sources to reject config apply"),e(Le.configStatus.lastError!==void 0,"Expected configStatus.lastError for invalid reference")}finally{Le.close()}}},{name:"kms unique slots stay distinct, alias slot 0, and raise demand on one resubscribe",fn:async()=>{let Ne=us({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()}]}}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("unique");e(ze!==void 0,"Expected the unique pool handle"),t(Ne.lastSubscribeOptions("/kms/unique")?.kmsSlots,1,"Expected the default subscribe demand to be 1"),t(ze.current(),"token-1","Expected current() to read the cached slot-0 grant");let De=ze.unique(3),Ve=ze.unique(3);e(De.length===3&&De.every((We,Ke)=>We===Ve[Ke]),"Expected repeated unique(n) calls to return the same slot handles"),e(De[0]!==ze,"Expected unique() slot handles to be distinct from the pool object"),t(await De[0]?.value(),"token-1","Expected unique()[0] to alias the pool slot-0 grant"),e(De[1]?.isReady()===!1,"Expected unfilled slot 1 to stay not ready"),e(De[2]?.isReady()===!1,"Expected unfilled slot 2 to stay not ready"),t(ze.current(),"token-1","Expected raising demand not to clear the slot-0 cache"),await Ns(De[1]?.ready(30),"Expected unfilled slot ready(timeout) to reject"),t(Ne.subscriptionCount("/kms/unique"),2,"Expected unique(3) to resubscribe exactly once"),t(Ne.lastSubscribeOptions("/kms/unique")?.kmsSlots,3,"Expected the resubscribe to carry demand 3");let Je=!1,Ge=De[1]?.value().then((We)=>(Je=!0,We));await wa(20),e(!Je,"Expected an unfilled unique() slot to wait for a later grant"),Ne.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()}]}),t(await Ge,"token-2","Expected a later multi-slot grant to fill waiting slots"),t(await De[2]?.value(),"token-3","Expected slot 2 to receive a distinct key"),t(await ze.value(),"token-1","Expected pool.value() to keep aliasing slot 0"),e(ze.accepts("token-1")&&ze.accepts("token-2")&&ze.accepts("token-3"),"Expected accepts() to span every delivered slot"),t(De[1]?.current(),"token-2","Expected slot current() after the grant"),t(Ne.subscriptionCount("/kms/unique"),2,"Expected later unique(2) not to resubscribe after demand 3"),e(ze.unique(2)[0]===De[0]&&ze.unique(2)[1]===De[1],"Expected a later smaller unique(n) to reuse the existing handles")}finally{Le.close()}}},{name:"kms current() is undefined before a grant and after de-configuration",fn:async()=>{let Ne=us({id:"kms-current",kms:{current:{path:"/kms/current"}}},{["/kms/current"]:ps("k1","token-1")},{delayInitialDelivery:!0}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("current");e(ze!==void 0,"Expected the current() pool handle"),e(ze.current()===void 0,"Expected current() to be undefined before the first grant"),Ne.deliverPool("/kms/current"),e(await Ts(()=>ze.current()==="token-1"),"Expected current() after grant delivery"),Ne.pushConfig({id:"kms-current"}),e(await Ts(()=>ze.current()===void 0),"Expected current() to clear on de-configuration")}finally{Le.close()}}},{name:"kms park() resolves the used value to a key id and never sends the secret",fn:async()=>{let Ne=us({id:"kms-park",kms:{park:{path:"/kms/park"}}},{["/kms/park"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("park");e(ze!==void 0,"Expected the park pool handle"),e(await Ts(()=>ze.current()==="token-1"),"Expected the grant before park");let De=await ze.park({value:"token-1",status:429,reason:"rate limited"});t(De.parked,!0,"Expected a successful park"),t(De.disposition,"cooldown","Expected 429 to map to cooldown on the fake source"),t(Ne.parkCalls().length,1,"Expected one outbound park"),t(Ne.parkCalls()[0]?.path,"/kms/park","Expected park to use the bound path"),t(Ne.parkCalls()[0]?.request.keyId,"k1","Expected value→id resolution"),t(Ne.parkCalls()[0]?.request.status,429,"Expected status to be forwarded"),t(Ne.parkCalls()[0]?.request.reason,"rate limited","Expected reason to be forwarded"),e(!JSON.stringify(Ne.parkCalls()[0]?.request).includes("token-1"),"Expected the used value never to appear on the outbound request");let Ve=await ze.park({value:"already-rotated"});t(Ve.parked,!1,"Expected a stale value not to park"),t(Ve.stale,!0,"Expected stale to be marked"),t(Ne.parkCalls().length,1,"Expected a stale value not to call the source");let Ge=await ze.unique(1)[0]?.park({value:"token-1",status:403});t(Ge?.parked,!0,"Expected slot.park to alias the pool"),t(Ne.parkCalls()[1]?.request.keyId,"k1","Expected slot park to resolve the same key"),t(Ne.parkCalls()[1]?.request.status,403,"Expected 403 to be forwarded from the slot alias")}finally{Le.close()}}},{name:"kms park() fails fast when the pool is not configured",fn:async()=>{let Ne=us({id:"kms-park-unconfigured",kms:{park:{path:"/kms/park-unconfigured"}}},{["/kms/park-unconfigured"]:ps("k1","token-1")}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("park");e(ze!==void 0,"Expected the park pool handle"),e(await Ts(()=>ze.current()==="token-1"),"Expected the grant before park");let De="";try{await ze.park({value:""})}catch(Je){De=Je instanceof Error?Je.message:String(Je)}e(De.includes("requires a non-empty value"),"Expected empty-value park() to fail fast on the configured handle"),t(Ne.parkCalls().length,0,"Expected empty-value park() not to call the source"),Ne.pushConfig({id:"kms-park-unconfigured"}),e(await Ts(()=>!ze.isReady()),"Expected the pool handle to tombstone");let Ve="";try{await ze.park({value:"token-1"})}catch(Je){Ve=Je instanceof Error?Je.message:String(Je)}e(Ve.includes("is not configured"),"Expected tombstoned park() to reject on the SDK handle"),t(Ne.parkCalls().length,0,"Expected tombstoned park() not to call the source")}finally{Le.close()}}},{name:"kms issuer addKeys and revokeKey forward and fail fast when unconfigured or empty",fn:async()=>{let Ne=us({id:"kms-issuer",kms:{issuer:{path:"/kms/issuer"}}},{["/kms/issuer"]:{kind:"kms",keys:[]}}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.issuer("issuer");e(ze!==void 0,"Expected the issuer handle"),e(ze.isReady(),"Expected issuer readiness to be independent of slot grants");let De=await ze.addKeys([{value:"oem-token"}]);t(De.keys[0]?.id,"issued-1","Expected the source to assign a key id"),t(Ne.issueCalls().length,1,"Expected one outbound addKeys"),t(Ne.issueCalls()[0]?.request.keys[0]?.value,"oem-token","Expected the issued value");let Ve=await ze.revokeKey({keyId:"issued-1",reason:"compromise"});t(Ve.revoked,!0,"Expected revoke to succeed"),t(Ne.revokeCalls()[0]?.request.keyId,"issued-1","Expected revoke to forward the key id"),await Ns(ze.addKeys([]),"Expected empty addKeys() to fail fast"),await Ns(ze.revokeKey({keyId:""}),"Expected empty-keyId revokeKey() to fail fast"),t(Ne.issueCalls().length,1,"Expected empty addKeys() not to call the source"),t(Ne.revokeCalls().length,1,"Expected empty revokeKey() not to call the source"),Ne.pushConfig({id:"kms-issuer"}),e(await Ts(()=>!ze.isReady()),"Expected issuer handle to tombstone"),await Ns(ze.addKeys([{value:"oem-token"}]),"Expected tombstoned addKeys() to reject"),t(Ne.issueCalls().length,1,"Expected tombstoned addKeys() not to call the source")}finally{Le.close()}}},{name:"kms issuer events() filters kms_* events and errors when the transport lacks subscribeEvents",fn:async()=>{let Ne=us({id:"kms-issuer-events",kms:{events:{path:"/kms/issuer-events"}}},{["/kms/issuer-events"]:{kind:"kms",keys:[]}},{events:!0}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let Ve=Le.kms?.issuer("events");e(Ve!==void 0,"Expected the issuer handle");let Je=[],Ge=Ve.events((We)=>{Je.push(`${We.event}:${We.keyId??""}`)});Ne.emitEvent({path:"/kms/issuer-events",event:"updated"}),Ne.emitEvent({path:"/other",event:"kms_key_parked",keyId:"other"}),Ne.emitEvent({path:"/kms/issuer-events",event:"kms_key_parked",keyId:"k1"}),Ne.emitEvent({path:"/kms/issuer-events",event:"kms_pool_low"}),t(Je.join(","),"kms_key_parked:k1,kms_pool_low:","Expected filtered issuer events"),Ge()}finally{Le.close()}let ze=us({id:"kms-issuer-no-events",kms:{events:{path:"/kms/issuer-events"}}},{["/kms/issuer-events"]:{kind:"kms",keys:[]}}),De=ds(ze,{validators:ls()});try{await De.ready(500);let Ve=De.kms?.issuer("events");e(Ve!==void 0,"Expected the issuer handle");let Je="";try{Ve.events(()=>{})}catch(Ge){Je=Ge instanceof Error?Ge.message:String(Ge)}e(Je.includes("events are not supported by this secrets transport"),"Expected events() to fail when the source lacks subscribeEvents")}finally{De.close()}}},{name:"kms verifier grants parse without slots, accept keys, and reject value/unique",fn:async()=>{let Ne=us({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()}]}}),Le=ds(Ne,{validators:ls()});try{await Le.ready(500);let ze=Le.kms?.getPool("verifier");e(ze!==void 0,"Expected the verifier pool handle"),e(await Ts(()=>ze.isReady()),"Expected a verifier grant with usable keys to be ready"),e(ze.accepts("token-1"),"Expected the active verifier key to be accepted"),e(ze.accepts("token-2"),"Expected the overlap-deprecated verifier key to be accepted"),e(!ze.accepts("token-other"),"Expected an unknown value to be rejected"),e(ze.current()===void 0,"Expected current() to stay undefined on a verifier grant"),await Ns(ze.value(30),"Expected value() to reject on a verifier grant");let De="";try{ze.unique(1)}catch(Ve){De=Ve instanceof Error?Ve.message:String(Ve)}e(De.includes("verifier grant has no slots"),"Expected unique() to reject")}finally{Le.close()}}}]}}async function fe(){let Be=performance.now(),Ne=[];for(let De of v().tests)Ne.push(await s(De.name,async()=>{await De.fn();return}));let Le=Ne.filter((De)=>!De.passed).length,ze=Ne.filter((De)=>De.details?.skipped).length;return{suite:"KMS",passed:Le===0,tests:Ne,duration:performance.now()-Be,summary:{total:Ne.length,passed:Ne.length-Le-ze,failed:Le,skipped:ze}}}var i="0.0.24-beta.20261007.23";var D=">=0.0.23-beta.20260809.2 <0.1.0",Q=[];function Mi(Be){let Ne=Be.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);return e(Ne!==null,`Invalid semantic version: ${Be}`),{major:Number(Ne[1]),minor:Number(Ne[2]),patch:Number(Ne[3]),prerelease:Ne[4]?Ne[4].split(".").map((Le)=>/^\d+$/.test(Le)?Number(Le):Le):[]}}function gu(Be,Ne){if(Be===Ne)return 0;if(typeof Be==="number"&&typeof Ne==="string")return-1;if(typeof Be==="string"&&typeof Ne==="number")return 1;return Be<Ne?-1:1}function fu(Be,Ne){let Le=Mi(Be),ze=Mi(Ne);for(let Ve of["major","minor","patch"])if(Le[Ve]!==ze[Ve])return Le[Ve]<ze[Ve]?-1:1;if(Le.prerelease.length===0&&ze.prerelease.length===0)return 0;if(Le.prerelease.length===0)return 1;if(ze.prerelease.length===0)return-1;let De=Math.max(Le.prerelease.length,ze.prerelease.length);for(let Ve=0;Ve<De;Ve++){let Je=Le.prerelease[Ve],Ge=ze.prerelease[Ve];if(Je===void 0)return-1;if(Ge===void 0)return 1;let We=gu(Je,Ge);if(We!==0)return We}return 0}function yu(Be,Ne){let Le=Ne.match(/^(>=|<=|>|<|=)?(.+)$/);e(Le!==null,`Invalid semantic version comparator: ${Ne}`);let ze=fu(Be,Le[2]);switch(Le[1]??"="){case">=":return ze>=0;case"<=":return ze<=0;case">":return ze>0;case"<":return ze<0;default:return ze===0}}function ee(Be,Ne=D){return Ne.split(/\s+/).filter(Boolean).every((Le)=>yu(Be,Le))}function Ni(Be){return`ECV(meta.run) ecv=${i} catalog=${Be}`}function Li(Be,Ne,Le){e(ee(Be.sdk.version),`Runtime SDK ${Be.sdk.version} is not compatible with ECV ${i} (${D})`),e(!Q.includes(Be.sdk.version),`Runtime SDK ${Be.sdk.version} is not accepted by ECV ${i}; upgrade the SDK`),Le?.annotate?.(Be.sdk.version,"ecv.sdk"),Le?.annotate?.(i,"ecv.version"),Le?.annotate?.(String(Be.sdk.catalogVersion),"ecv.catalog"),Le?.annotate?.([...Ne].sort().join(","),"ecv.claims")}function zi(Be,Ne,Le){let ze=new Set(Ne),De=Ne.filter((Je)=>Be.capabilities[Je]?.testable!==!0);e(De.length===0,`Claimed capabilities are not testable: ${De.map((Je)=>`${Je} (${Be.capabilities[Je]?.reason??"no manifest state"})`).join("; ")}`);let Ve=Object.keys(Be.capabilities).filter((Je)=>Be.capabilities[Je].testable&&!ze.has(Je));if(Ve.length>0)Le?.annotate?.(`Runtime capabilities not claimed by this suite: ${Ve.join(",")}`,"ecv.unclaimed")}import{createServer as wu}from"node:http";import{IoniteError as vu,ionite as ra}from"@ionite/server";import{createValidators as ia}from"@ionite/valibot";var rr=(Be)=>new Promise((Ne)=>setTimeout(Ne,Be));async function ea(Be,Ne=1500,Le=20){let ze=Date.now()+Ne;while(Date.now()<ze){if(Be())return!0;await rr(Le)}return Be()}async function bu(){let Be=[],Ne=wu((ze,De)=>{let Ve=[];ze.on("data",(Je)=>Ve.push(Je)),ze.on("end",()=>{Be.push({path:ze.url??"",headers:ze.headers,body:Buffer.concat(Ve)}),De.statusCode=200,De.end()})});await new Promise((ze,De)=>{Ne.once("error",De),Ne.listen(0,"127.0.0.1",ze)});let Le=Ne.address();if(!Le||typeof Le==="string")throw Error("OTLP recorder did not bind a TCP port");return{endpoint:`http://127.0.0.1:${Le.port}`,requests:Be,close:()=>new Promise((ze,De)=>Ne.close((Ve)=>Ve?De(Ve):ze()))}}function va(Be){let Ne=Be,Le;return{async load(){return Ne},async subscribe(ze){return Le=ze,()=>{Le=void 0}},push(ze){Ne=ze,Le?.(ze)},secret:{}}}function ku(Be){let Le=Be,ze=1,De=new Set,Ve=()=>({data:structuredClone(Le),metadata:{path:"/kms/otel-ecv",version:ze,created:new Date(0)}});return{secret:{type:"dev",isReady:()=>!0,ready:async()=>{},get:()=>{throw Error("not implemented")},getFullSecret:async()=>Ve(),getSecret:async()=>structuredClone(Le),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(Ge,We)=>(De.add(We),We(Ve()),()=>De.delete(We))},async load(){return{id:"otel-kms",kms:{"otel-ecv":{path:"/kms/otel-ecv"}},otel:{...Oa,apiKeyPool:"otel-ecv"}}},async subscribe(){return()=>{}},pushPool(Ge){Le=Ge,ze+=1;for(let We of De)We(Ve())}}}var Oa={name:"ecv-otel",traces:!1,metrics:!1,logs:!1};function Su(){return{name:"OTLP logs export with console off and preserve HTTP/protobuf headers",fn:async()=>{let Be=await bu(),Ne=[],Le=va({id:"otel-protobuf-wire",otel:{name:"ecv-otel-wire",endpoint:Be.endpoint,headers:{"x-ecv-api-key":"wire-secret"},console:"off",traces:!1,metrics:!1,logs:!0}}),ze=ra(Le,{validators:ia(),otel:{console:(De)=>Ne.push(De)}});try{await ze.ready(1000),e(await ea(()=>ze.otel?.isReady?.()===!0),"Expected OTel ready before emitting a log"),ze.otel?.logger.info("ecv protobuf wire marker",{"ecv.marker":"protobuf"}),await ze.otel?.forceFlush?.(),e(await ea(()=>Be.requests.length>0),"Expected the OTLP collector to receive a log");let De=Be.requests[0];e(De?.path==="/v1/logs",`Expected OTLP logs path, got ${De?.path??"no request"}`);let Ve=String(De?.headers["content-type"]??"");e(Ve.includes("application/x-protobuf"),`Expected protobuf content type, got ${Ve||"none"}`),e(De?.headers["x-ecv-api-key"]==="wire-secret","Expected configured exporter header"),e((De?.body.byteLength??0)>0,"Expected a non-empty OTLP protobuf body"),t(Ne.length,0,"Expected console off to suppress the framework console sink")}finally{await ze.otel?.shutdown?.(),ze.close(),await Be.close()}}}}function Tu(){return{name:"OTel console policy applies a severity floor and pretty one-line format",fn:async()=>{let Be=[],Ne=globalThis.console.warn;globalThis.console.warn=(...De)=>{Be.push(De.map(String).join(" "))};let Le=va({id:"otel-console-policy",otel:{...Oa,console:"warn"}}),ze=ra(Le,{validators:ia()});try{await ze.ready(1000),e(await ea(()=>ze.otel?.isReady?.()===!0),"Expected OTel ready before emitting logs"),ze.otel?.logger.info("ecv console info marker",{"ecv.marker":"filtered"}),ze.otel?.logger.warn(`ecv console warn marker
|
|
2
|
+
continued`,{"ecv.marker":"pretty",count:1});let De=Be.filter((Je)=>Je.includes("ecv console"));t(De.length,1,"Expected the warn floor to suppress info and emit warn");let Ve=De[0]??"";e(!Ve.includes(`
|
|
3
|
+
`),"Expected console output to remain on one line"),e(/^\d{4}-\d{2}-\d{2}T[^ ]+ WARN {2}ecv console warn marker\\ncontinued /.test(Ve),`Expected timestamped WARN pretty prefix, got ${Ve}`),e(Ve.includes("ecv.marker=pretty"),"Expected logfmt-style structured attributes"),e(Ve.includes("count=1"),"Expected numeric structured attributes"),e(!Ve.includes('{"ecv.marker"'),"Expected attributes not to render as a JSON object")}finally{globalThis.console.warn=Ne,ze.close()}}}}function b(){return{tests:[{name:"otel module is inactive (not-ready) when RemoteConfig omits otel (§22.2)",fn:async()=>{let Ne=va({id:"otel-off"}),Le=ra(Ne,{validators:ia()});try{await Le.ready(500),e(Le.otel?.isReady?.()!==!0,"Expected ion.otel to be not-ready with no RemoteConfig.otel");let ze=Le.readiness();t(ze.modules.otel.active,!1,"Expected modules.otel.active === false when unconfigured");let De=Le.requireReady(["otel"]);e(De.ok===!1,'Expected requireReady(["otel"]) to fail when otel is unconfigured'),e(De.notReady.some((Ve)=>Ve.name==="otel"),"Expected otel to be listed among the not-ready dependencies")}finally{Le.close?.()}}},...process.env.ECV_OTEL_WIRE==="1"?[Su()]:[],Tu(),{name:"RemoteConfig.otel activates the module; de-configuration re-arms readiness (§22.2)",fn:async()=>{let Ne=va({id:"otel-rearm",otel:{...Oa}}),Le=ra(Ne,{validators:ia()});try{await Le.ready(1000),e(Le.otel?.isReady?.()===!0,"Expected ion.otel.isReady() true after RemoteConfig.otel applied");let ze=Le.readiness();t(ze.modules.otel.active,!0,"Expected modules.otel.active === true once configured"),t(ze.modules.otel.ready,!0,"Expected modules.otel.ready === true once started"),e(Le.requireReady(["otel"]).ok===!0,'Expected requireReady(["otel"]) to pass when active+ready'),Ne.push({id:"otel-rearm"}),await ea(()=>Le.otel?.isReady?.()!==!0),e(Le.otel?.isReady?.()!==!0,"Expected ion.otel.isReady() false after de-configuration");let De=Le.readiness();t(De.modules.otel.active,!1,"Expected modules.otel.active === false after de-config"),e(Le.requireReady(["otel"]).ok===!1,'Expected requireReady(["otel"]) to fail once otel is de-configured');let Ve=!1;try{await Le.otel?.ready?.(50)}catch{Ve=!0}e(Ve,"Expected a re-armed ion.otel.ready(50) to reject while de-configured");let Je={resolved:!1},Ge=Le.otel?.ready?.().then(()=>{Je.resolved=!0}).catch(()=>{});await rr(60);let We=Je.resolved===!1;e(We,"Expected a fresh no-timeout ready() to stay pending while inactive"),Ne.push({id:"otel-rearm",otel:{...Oa}}),await ea(()=>Le.otel?.isReady?.()===!0),await Ge;let Ke=Je.resolved===!0;e(Ke,"Expected the re-armed ready() waiter to resolve after re-activation"),e(Le.otel?.isReady?.()===!0,"Expected ion.otel.isReady() true after re-activation"),e(Le.requireReady(["otel"]).ok===!0,'Expected requireReady(["otel"]) to pass again after re-activation')}finally{Le.close?.()}}},{name:"otel rebuilds exporters when its referenced KMS key rotates",fn:async()=>{let Ne=ku({kind:"kms",slots:["k1"],keys:[{id:"k1",state:"active",value:"token-1",notBefore:new Date(0).toISOString()}]}),Le=ra(Ne,{validators:ia()});try{await Le.ready(1000),e(await ea(()=>Le.otel?.isReady?.()===!0),"Expected OTel to start with KMS token");let ze=0,De=Le.otel?.afterChange?.(()=>{ze+=1});Ne.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()}]}),e(await ea(()=>ze>0),"Expected KMS active-key change to rebuild managed OTel"),e(await ea(()=>Le.otel?.isReady?.()===!0),"Expected OTel ready after credential rebuild"),t(ze,1,"Expected the active-key rotation to rebuild managed OTel exactly once");let Ve=Le.kms?.getPool("otel-ecv");e(Ve?.accepts("token-1")===!0,"Expected OTel KMS overlap to accept the deprecated key"),e(Ve.accepts("token-2"),"Expected OTel KMS overlap to accept the new active key"),t(await Ve.value(),"token-2","Expected OTel KMS value() to expose the rotated active key"),Ne.pushPool({kind:"kms",slots:["k2"],keys:[{id:"k2",state:"active",value:"token-2",notBefore:new Date(0).toISOString()}]}),e(!Ve.accepts("token-1"),"Expected overlap removal to stop accepting the deprecated key"),await rr(80),t(ze,1,"Expected overlap-only cleanup not to rebuild OTel when the active key is unchanged"),e(Le.otel?.isReady?.()===!0,"Expected OTel to remain ready after overlap-only cleanup"),De?.()}finally{Le.close()}}},{name:"an invalid RemoteConfig.otel provider config fails the config apply (§22.2)",fn:async()=>{let Ne=va({id:"otel-invalid",otel:{type:"datadog",name:"ecv-otel",traces:!1,metrics:!1,logs:!1}}),Le=ra(Ne,{validators:ia()});try{let ze=!1;try{await Le.ready(500)}catch{ze=!0}e(ze,"Expected an invalid otel provider config to reject readiness at apply"),e(Le.configStatus?.lastError!==void 0,"Expected configStatus.lastError to record the failed otel apply")}finally{Le.close?.()}}},{name:"a tenant runtime failure record carries ion.id and stays on that runtime log stream",fn:async()=>{let Ne=[],Le=[],ze=ra(va({id:"otel-root-runtime",otel:{...Oa,console:"error"}}),{validators:ia(),otel:{console:(Ve)=>Ne.push(Ve)}}),De=ra(va({id:"otel-tenant-runtime",iam:{users:{assignment:"iam"}},otel:{...Oa,name:"ecv-otel-tenant",console:"error"}}),{validators:ia(),otel:{console:(Ve)=>Le.push(Ve)}});try{await ze.ready(1000),await De.ready(1000),e(await ea(()=>De.otel?.isReady?.()===!0),"Expected the tenant OTel module to be ready");try{await De.authz.grant({subject:"user-1",subjectType:"user",targetType:"group",target:"admins"}),e(!1,"grant should reject when assignment is iam")}catch(Je){e(Je instanceof vu,"grant failure is an IoniteError"),t(Je.ionId,"otel-tenant-runtime","the error is stamped with the tenant runtime"),Je.log?.error("authz.grant failed",Je.toLogAttributes(),Je)}let Ve=Le.find((Je)=>Je.attributes?.["ion.id"]==="otel-tenant-runtime");e(Ve!==void 0,"the tenant log stream received the failure"),e(Ne.every((Je)=>Je.attributes?.["ion.id"]!=="otel-tenant-runtime"),"the root log stream did not receive the tenant failure")}finally{ze.close(),De.close()}}}]}}async function ye(){let Be=performance.now(),{tests:Ne}=b(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"OpenTelemetry",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{IoniteError as Fn,ionite as ta,vault as Cu}from"@ionite/server";import{createValidators as sa}from"@ionite/valibot";var ca="/api/ionite",da="http://localhost";function la(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}async function aa(Be){let Ne=await Be.text();try{return JSON.parse(Ne)}catch{throw Error(`Expected a JSON body, got: ${Ne.slice(0,120)}`)}}function k(){return{tests:[{name:"POST {basePath}/readyz returns a structured 405 with an Allow header (§11.2)",fn:async()=>{let Be=ta(la({id:"protocol-readyz-405"}),{validators:sa()});await Be.ready(1000);let Ne=await Be.handler(new Request(`${da}${ca}/readyz`,{method:"POST"}));t(Ne.status,405,"POST to readyz must be 405 Method Not Allowed"),e(Ne.headers.get("Allow")?.includes("GET")??!1,"Allow header must advertise GET");let Le=await aa(Ne);t(Le.error,"method_not_allowed","body carries the stable method_not_allowed code"),Be.close?.()}},{name:"GET {basePath}/readyz is reachable and reports config applied",fn:async()=>{let Be=ta(la({id:"protocol-readyz-get"}),{validators:sa()});await Be.ready(1000);let Ne=await Be.handler(new Request(`${da}${ca}/readyz`));e(Ne.status===200||Ne.status===503,"readyz GET returns a readiness status");let Le=await aa(Ne);t(Le.applied,!0,"readyz reports config applied after ion.ready()"),Be.close?.()}},{name:"POST {basePath}/capabilitiez returns 405 before authentication (§11.2)",fn:async()=>{let Be=ta(la({id:"protocol-cap-405",capabilitiez:!0}),{validators:sa()});await Be.ready(1000);let Ne=await Be.handler(new Request(`${da}${ca}/capabilitiez`,{method:"POST"}));t(Ne.status,405,"POST to capabilitiez must be 405");let Le=await aa(Ne);t(Le.error,"method_not_allowed","capabilitiez 405 carries the stable code"),Be.close?.()}},{name:"GET {basePath}/capabilitiez returns JSON 404 when disabled (§11.2)",fn:async()=>{let Be=ta(la({id:"protocol-cap-disabled"}),{validators:sa()});await Be.ready(1000);let Ne=await Be.handler(new Request(`${da}${ca}/capabilitiez`));t(Ne.status,404,"disabled capabilitiez is 404");let Le=await aa(Ne);t(Le.error,"not_found","disabled capabilitiez returns the stable JSON not_found code"),Be.close?.()}},{name:"GET {basePath}/capabilitiez returns 503 before config is applied",fn:async()=>{let Be,Le=ta({load:()=>new Promise((Ve)=>{Be=Ve}),async subscribe(){return},secret:{}},{validators:sa()}),ze=await Le.handler(new Request(`${da}${ca}/capabilitiez`));t(ze.status,503,"capabilitiez must not expose a manifest before config is applied");let De=await aa(ze);t(De.error,"ionite is not ready","not-ready capabilitiez returns the stable error code"),Be?.({id:"protocol-cap-not-ready",capabilitiez:!0}),await Le.ready(1000),Le.close?.()}},{name:"GET {basePath}/capabilitiez requires workload authentication when enabled",fn:async()=>{let Be=ta(la({id:"protocol-cap-auth",capabilitiez:!0}),{validators:sa()});await Be.ready(1000);let Ne=await Be.handler(new Request(`${da}${ca}/capabilitiez`));t(Ne.status,401,"enabled capabilitiez must reject an unauthenticated caller"),Be.close?.()}},{name:"an unknown ionite route returns JSON 404 { error: not_found } (§9.2)",fn:async()=>{let Be=ta(la({id:"protocol-unknown-404"}),{validators:sa()});await Be.ready(1000);let Ne=await Be.handler(new Request(`${da}${ca}/does-not-exist`));t(Ne.status,404,"unknown route is 404");let Le=await aa(Ne);t(Le.error,"not_found","unknown route returns the stable JSON not_found code"),Be.close?.()}},{name:"a throwing handler is caught and sanitized to 500 { error: internal_error } (§9.2)",fn:async()=>{let Ne=ta(la({id:"protocol-throw-500"}),{validators:sa(),routing:{resolve:()=>({module:"sso",kind:"sso.handler",handler:async()=>{throw Error("LEAKED_INTERNAL_DETAIL_42")}})}});await Ne.ready(1000);let Le=await Ne.handler(new Request(`${da}${ca}/anything`));t(Le.status,500,"an unhandled handler throw becomes a 500");let ze=await Le.text();e(!ze.includes("LEAKED_INTERNAL_DETAIL_42"),"the raw error message must not leak to the caller");let De=JSON.parse(ze);t(De.error,"internal_error","a thrown error collapses to the stable internal_error code"),Ne.close?.()}},{name:"IoniteError.response preserves deliberate status metadata and hides the server message",fn:async()=>{let Be=new Fn("Internal duplicate detail",{status:409,statusText:"tenant_exists",detail:"Tenant already exists",upstreamStatus:404}),Ne=Be.response(),Le=await aa(Ne);t(Ne.status,409,"IoniteError status is the client status"),t(Le.error,"tenant_exists","statusText is the client code"),t(Le.detail,"Tenant already exists","detail is the client explanation"),e(!("message"in Le),"the server message is not in the body"),e(!("upstreamStatus"in Le),"the upstream status is not in the body");let ze=JSON.stringify(Be);e(!ze.includes("Internal duplicate detail"),"toJSON omits the server message"),e(!ze.includes("404"),"toJSON omits the upstream status");let De=Be.response(new Request("https://app.example/",{headers:{accept:"text/html"}}));t(De.headers.get("content-type"),"text/plain; charset=utf-8","HTML Accept gets plain text"),t(await De.text(),"Tenant already exists","HTML body is the detail");let Ve=Be.response(new Request("https://app.example/",{headers:{accept:"text/html, application/json"}}));e((Ve.headers.get("content-type")??"").startsWith("application/json"),"an Accept that includes JSON stays JSON")}},{name:"a vault 404 is a client 503 and keeps the upstream status off the response",fn:async()=>{let Be=Cu({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),Ne=globalThis.fetch;globalThis.fetch=async()=>new Response("{}",{status:404});let Le;try{await Be.getSecret("app/config")}catch(De){Le=De}finally{globalThis.fetch=Ne,Be.close?.()}e(Le instanceof Fn,"a vault read failure is an IoniteError"),t(Le.status,503,"the client status is unavailable, not the vault status"),t(Le.upstreamStatus,404,"the vault status stays on the error"),t(Le.toLogAttributes()["ion.upstream.status"],404,"logs carry the vault status");let ze=await aa(Le.response());t(ze.error,"internal_error","a transport error has no client code"),e(!("upstreamStatus"in ze),"the vault status is not returned"),e(!JSON.stringify(ze).includes("app/config"),"the vault path is not returned")}},{name:"IoniteError.from does not reflect an unknown upstream status",fn:async()=>{let Be=Object.assign(TypeError("Vault denied the write"),{status:403}),Ne=Fn.from(Be),Le=Ne.response(),ze=await aa(Le);t(Le.status,500,"an upstream transport status is not App response policy"),t(ze.error,"internal_error","the upstream detail is sanitized"),e(!JSON.stringify(ze).includes("Vault denied"),"the upstream message is not returned");let De=Ne.toLogAttributes();t(De["exception.type"],"TypeError","the log record names the original error type"),t(De["exception.stacktrace"],Be.stack,"the log record keeps the original stack")}},{name:"a thrown module error is stamped with its runtime and logged by the handler catcher",fn:async()=>{let Be=[],Ne={trace(){},debug(){},info(){},warn(){},error(...Ve){Be.push(Ve)}},Le=ta(la({id:"protocol-stamped-error",iam:{users:{assignment:"iam"}}}),{validators:sa(),log:Ne});await Le.ready(1000);let ze=Le.authz.grant;e(ze===Le.authz.grant,"a stamped module method keeps its identity across reads");try{await Le.authz.grant({subject:"user-1",subjectType:"user",targetType:"group",target:"admins"}),e(!1,"grant should reject when assignment is iam")}catch(Ve){e(Ve instanceof Fn,"module throws are IoniteError"),t(Ve.ionId,"protocol-stamped-error","the error carries the runtime id"),e(Ve.log===Le.log,"the error carries the runtime logger"),Ve.log?.error("Request failed.",Ve.toLogAttributes(),Ve)}t(Be.length,1,"the catcher logs the failure once");let De=Be[0]?.[1];t(De["ion.id"],"protocol-stamped-error","log attributes include the runtime id"),t(De["exception.type"],"AssignmentPolicyError","log attributes include the error type"),Le.close?.()}}]}}async function we(){let{tests:Be}=k(),Ne=[];for(let Le of Be)try{await Le.fn(),Ne.push({name:Le.name,passed:!0})}catch(ze){Ne.push({name:Le.name,passed:!1,error:ze instanceof Error?ze.message:String(ze)})}return Ne}import{CONTENT_TYPE_JSON as Ji,defineSecretType as Ru,JSON_MEDIA_TYPE as Di,SecretContentTypeMismatchError as xu,waitOn as Eu}from"@ionite/core";import{generateLfvEd25519KeyPair as ir,signLfvPayload as Gi,stringifyLfvJwks as cr}from"@ionite/core/server";import{lfvVault as dr,subscribeSecretPathWithHttpPolling as Au,vault as lr}from"@ionite/server";var Is={lfvServerUrl:"http://localhost:3599/api/lfv",clientId:"ecv-hardening-client",path:"/ecv/hardening/config",deliveryEndpoint:"/api/ionite/lfv/delivery",eventsEndpoint:"/api/ionite/lfv/events"},Ls=(Be)=>new Promise((Ne)=>setTimeout(Ne,Be));async function Vi(Be,Ne,Le,ze){let De=String(Ne.request_id),Ve="issued_at"in Ne?Ne:{...Ne,issued_at:Date.now()},Je=JSON.stringify(Ve),Ge=await Gi({privateKeyJwk:Le,requestId:De,rawBody:Je});return new Request(`http://localhost${Be}`,{method:"POST",headers:{...Ji,"X-LFV-Request-Id":De,"X-LFV-Key-Id":ze,"X-LFV-Signature":Ge},body:Je})}async function Iu(Be,Ne,Le){let ze=String(Be.request_id),De=JSON.stringify(Be),Ve=await Gi({privateKeyJwk:Ne,requestId:ze,rawBody:De});return new Response(De,{status:200,headers:{...Ji,"X-LFV-Request-Id":ze,"X-LFV-Key-Id":Le,"X-LFV-Signature":Ve}})}function Bi(Be){if(typeof Be==="string")return Be;if(Be instanceof URL)return Be.toString();return Be.url}function Pu(Be,Ne=5){let Le=[],ze=Be;for(let De=0;De<Ne&&ze!=null;De+=1)if(ze instanceof Error)Le.push(ze.message),ze=ze.cause;else{Le.push(String(ze));break}return Le.join(" <- ")}function _u(Be,Ne){let Le=Be?.headers;if(!Le)return null;if(Le instanceof Headers)return Le.get(Ne);if(Array.isArray(Le)){let De=Le.find(([Ve])=>Ve.toLowerCase()===Ne.toLowerCase());return De?De[1]:null}let ze=Le;for(let[De,Ve]of Object.entries(ze))if(De.toLowerCase()===Ne.toLowerCase())return Ve;return null}function S(){return{tests:[{name:"upstream error bodies are redacted from thrown errors (no secret leaks into configStatus.lastError)",fn:async()=>{let Le=lr({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),ze=globalThis.fetch;globalThis.fetch=async()=>new Response(JSON.stringify({errors:["SECRET-DO-NOT-LEAK-7f3a91"],leaked:"SECRET-DO-NOT-LEAK-7f3a91"}),{status:500,statusText:"Internal Server Error"});try{let De=!1,Ve="";try{await Le.getSecret("app/config")}catch(Je){De=!0,Ve=Pu(Je)}e(De,"a non-200 vault read must throw (fail closed), not return stale/empty data"),e(!Ve.includes("SECRET-DO-NOT-LEAK-7f3a91"),`thrown error must not include the upstream body (got: ${Ve})`),e(Ve.includes("500"),`redacted error should still carry the structured status (got: ${Ve})`)}finally{globalThis.fetch=ze,Le.close?.()}}},{name:"vault HTTP requests accept JSON and warn with a redacted HTML title on a non-JSON success",fn:async()=>{let Le=[],De=lr({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token",log:{trace:()=>{},debug:()=>{},info:()=>{},warn:(Ge,...We)=>{if(We[0]&&typeof We[0]==="object")Le.push(We[0])},error:()=>{}}}),Ve=globalThis.fetch,Je=null;globalThis.fetch=async(Ge,We)=>(Je=new Headers(We?.headers).get("Accept"),new Response("<html><title>Origin tunnel unavailable</title><body>SECRET-BODY-MUST-NOT-BE-LOGGED</body></html>",{status:200,headers:{"Content-Type":"text/html; charset=UTF-8"}}));try{let Ge;try{await De.getSecret("app/config")}catch(Ke){Ge=Ke}e(Ge instanceof Error&&Ge.name==="VaultRequestError","a non-JSON vault response must fail as VaultRequestError"),e(Je===Di,`vault request must send ${Di}, got ${Je}`);let We=Le.find((Ke)=>Ke.reason==="non_json_response");e(We?.excerpt==="Origin tunnel unavailable"&&JSON.stringify(We).includes("SECRET-BODY-MUST-NOT-BE-LOGGED")===!1,"the warning includes only the HTML title and never the response body")}finally{globalThis.fetch=Ve,De.close?.()}}},{name:"http polling never overlaps and surfaces degraded/recovered health",fn:async()=>{let Ne=0,Le=0,ze=0,De=!1,Ve=[],Ge=await Au({getFullSecret:async(Ke)=>{if(Ne+=1,Le=Math.max(Le,Ne),ze+=1,await Ls(25),Ne-=1,De)throw Error("simulated upstream failure");return{data:{},metadata:{path:Ke,version:1,created:new Date}}}},"/p",()=>{},15,void 0,void 0,{onHealth:(Ke)=>Ve.push(Ke)});await Ls(180),t(Le,1,"HTTP polls must never overlap (self-scheduling chain)"),De=!0,await Ls(120),e(Ve.includes(!1),"a failed poll must surface unhealthy (degraded isAvailable) instead of hammering"),De=!1,await Ls(120),t(Ve[Ve.length-1],!0,"recovery must surface healthy again"),Ge();let We=ze;await Ls(120),t(ze,We,"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 Ne=lr({type:"vault",url:"http://vault.test/v1/secret/data",token:"ecv-test-token"}),Le=globalThis.fetch;globalThis.fetch=async(ze)=>{let De=new URL(Bi(ze)).pathname.split("/").pop(),Ve={path:`/${De}`,version:1,created_time:new Date(0).toISOString()};if(De==="env")return Response.json({data:{data:{API_KEY:"secret",PORT:"443"},metadata:{...Ve,content_type:{format:"env",mime_type:"text/x-dotenv"}}}});if(De==="text")return Response.json({data:{data:{content:"hello"},metadata:{...Ve,content_type:{format:"text",mime_type:"text/plain"}}}});if(De==="binary")return Response.json({data:{data:{content_base64:"AAEC/w=="},metadata:{...Ve,content_type:{format:"binary",mime_type:"application/octet-stream"}}}});return Response.json({data:{data:{legacy:!0},metadata:Ve}})};try{let ze=await Ne.get("env").env();t(ze.API_KEY,"secret","env() must decode the flat key/value object"),t(ze.PORT,"443","env() must preserve string values"),t(await Ne.get("text").text(),"hello","text() must unwrap the content field");let De=await Ne.get("binary").bytes();t(Array.from(De).join(","),"0,1,2,255","bytes() must fully decode base64");let Ve=await Ne.get("legacy").json();t(Ve.legacy,!0,"legacy secrets without content_type must default to JSONC");let Je=Ru({id:"expected-json",format:"jsonc"}),Ge;try{await Ne.get("text").parse(Je)}catch(Ke){Ge=Ke}e(Ge instanceof xu,"parse() must throw the structured mismatch error before decoding");let We=Ge;t(We.expected.mimeType,"application/json","mismatch must expose expected MIME type"),t(We.received.mimeType,"text/plain","mismatch must expose received MIME type")}finally{globalThis.fetch=Le,Ne.close?.()}}},{name:"LFV OTP-as-possession-proof: no client secret, OTP arrives only via signed callback",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await ir("ecv-otp-kid"),ze=dr({...Is,verifyJwks:cr(Le)}),De=0,Ve=0,Je=!1,Ge=7,We=globalThis.fetch;globalThis.fetch=async(Ke,Fe)=>{let He=Bi(Ke);if(_u(Fe,"X-LFV-Signature")!==null)Je=!0;let Xe=JSON.parse(String(Fe?.body??"{}"));if(He.endsWith("/otp"))return De+=1,await ze.handleDelivery(await Vi(Is.deliveryEndpoint,{request_id:Xe.request_id,type:"otp",otp:"otp-value",path:Is.path,action:Xe.action},Ne,"ecv-otp-kid")),new Response(JSON.stringify({request_id:Xe.request_id,status:"pending"}),{status:202});if(He.endsWith("/secrets/request"))return Ve+=1,Iu({request_id:Xe.request_id,status:"completed",path:Is.path,data:{data:{marker:"possession-ok"},metadata:{path:Is.path,version:Ge,content_type:{format:"jsonc",mime_type:"application/vnd.ionite.ecv+json",schema_url:"https://example.com/ecv.schema.json",type_id:"ecv-payload"}}}},Ne,"ecv-otp-kid");return new Response("{}",{status:200})};try{let Ke=await Promise.all(Array.from({length:5},()=>ze.getSecret(Is.path)));for(let He of Ke)t(He.marker,"possession-ok","each concurrent read should resolve from the one cycle");let Fe=await ze.getFullSecret(Is.path);t(Fe.metadata.contentType?.format,"jsonc","LFV delivery must preserve the secret format"),t(Fe.metadata.contentType?.mimeType,"application/vnd.ionite.ecv+json","LFV delivery must preserve the MIME type"),t(Fe.metadata.contentType?.schemaUrl,"https://example.com/ecv.schema.json","LFV delivery must preserve the schema URL"),t(Fe.metadata.contentType?.typeId,"ecv-payload","LFV delivery must preserve the type id"),t(De,1,"concurrent reads must issue exactly one OTP request (single-flight)"),t(Ve,1,"concurrent reads must issue exactly one secret request (single-flight)"),e(!Je,"client must not send X-LFV-Signature (no client-held secret)")}finally{globalThis.fetch=We,ze.close?.()}}},{name:"self-redeeming LFV event storms single-flight OTP issuance per path",fn:async()=>{let{privateJwk:Ne,publicJwks:Le}=await ir("ecv-event-flight-kid"),ze=dr({...Is,verifyJwks:cr(Le),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),De=globalThis.fetch,Ve=0,Je,Ge=new Promise((We)=>{Je=We});globalThis.fetch=async()=>(Ve+=1,Ge);try{for(let We=0;We<5;We+=1){let Ke=await ze.handleEvents(await Vi(Is.eventsEndpoint,{request_id:crypto.randomUUID(),path:Is.path,event:"updated"},Ne,"ecv-event-flight-kid"));t(Ke.status,200,"event callback is accepted")}await Ls(20),t(Ve,1,"concurrent event refreshes issue one OTP request for the path"),Je?.(Response.json({status:"pending"},{status:202})),await Ls(20)}finally{Je?.(Response.json({status:"pending"},{status:202})),globalThis.fetch=De,ze.close?.()}}},{name:"LFV close() mid-outage settles pending waiters instead of leaking them",fn:async()=>{let{publicJwks:Ne}=await ir("ecv-close-kid"),Le=dr({...Is,verifyJwks:cr(Ne)}),ze=globalThis.fetch;globalThis.fetch=async(De,Ve)=>{let Je=JSON.parse(String(Ve?.body??"{}"));return new Response(JSON.stringify({request_id:Je.request_id,status:"pending"}),{status:202})};try{let De=Le.getSecret(Is.path),Ve=!1;De.catch(()=>{Ve=!0}),await Ls(40),Le.close?.();let Je=!1,Ge="";try{await De}catch(We){Je=!0,Ge=We instanceof Error?We.message:String(We)}e(Je,"close() must settle the pending read (reject), not leak the waiter"),e(Ge.toLowerCase().includes("closed"),`rejection should explain the source closed (got: ${Ge})`),e(Ve,"pending waiter promise must settle after close()")}finally{globalThis.fetch=ze}}},{name:"waitOn probes never overlap and stop after success (timers cleared)",fn:async()=>{let Ne=0,Le=0,ze=0,De=!1,Ve=globalThis.fetch;globalThis.fetch=async()=>(Ne+=1,Le+=1,ze=Math.max(ze,Le),await Ls(40),Le-=1,new Response("{}",{status:200}));let Je=setTimeout(()=>{De=!0},150);try{await Eu("http://localhost/ecv/health",()=>De,10,0,5000),t(ze,1,"waitOn must never run overlapping probes (self-scheduling chain)"),e(Ne>=2,"the probe chain should have iterated at least twice before becoming ready");let Ge=Ne;await Ls(120),t(Ne,Ge,"no probe may fire after waitOn resolves (retry timer cleared)")}finally{clearTimeout(Je),globalThis.fetch=Ve}}}]}}async function ve(){let Be=performance.now(),{tests:Ne}=S(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Secrets hardening",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{defineCatalog as ju,getSessionEntries as ln,InMemoryCustomerStore as Uu,InMemorySessionStore as Hn,inMemoryAuthzStores as Ou,ionite as qu,materializeTenantOwnerEntitlement as $u,tenantOwnerEntitlementId as Mu,tenantOwnerPermission as Nu,tenantResourceId as Lu}from"@ionite/server";import{createValidators as zu}from"@ionite/valibot";function Du(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function Wi(Be){return{sid:Be.sid,sub:`sub-${Be.email}`,authType:"sso",tenantId:Be.tenantId,createdAt:new Date,expiresAt:Be.expiresAt,claims:{sub:`sub-${Be.email}`,name:Be.name,email:Be.email}}}async function Yn(Be){return await Be.create(Wi({sid:"sid-alpha",tenantId:"alpha",expiresAt:new Date(Date.now()+7200000),name:"Ada",email:"ada@example.com"})),await Be.create(Wi({sid:"sid-beta",tenantId:"beta",expiresAt:new Date(Date.now()+3600000),name:"Ada",email:"ada@example.com"})),await Be.create({sid:"sid-gamma",sub:"cust-9",authType:"ciam",tenantId:"gamma",createdAt:new Date,expiresAt:new Date(Date.now()+1800000),claims:{sub:"cust-9",name:"Cus Tomer",email:"cus@example.com"}}),"ion.sso.alpha.session=sid-alpha; ion.sso.beta.session=sid-beta; ion.ciam.gamma.session=sid-gamma"}function T(){return{tests:[{name:"lists live sessions newest window first with labels and identity groups",fn:async()=>{let Ne=new Hn,Le=await Yn(Ne),{tenants:ze,groups:De}=await ln(new Request("https://app.ecv.local/",{headers:{cookie:Le}}),{sessionStore:Ne,tenantLabel:(Ve)=>`Tenant ${Ve}`});e(ze.map((Ve)=>Ve.tenantId).join(",")==="alpha,beta,gamma",`entries are newest session window first, got ${ze.map((Ve)=>Ve.tenantId).join(",")}`),e(ze.every((Ve)=>Ve.source==="session"),"cookie-discovered entries carry source session"),e(ze[0]?.tenantName==="Tenant alpha","tenantLabel resolves per discovered id"),e(ze[0]?.user.email==="ada@example.com","claims project into the display user"),e(ze[2]?.authType==="ciam","CIAM sessions keep their auth type"),e(ze.every((Ve)=>typeof Ve.expiresAt==="string"),"session entries expose the window expiry for client ordering"),e(De.length===2,`one group per identity, got ${De.length}`),e(De.find((Ve)=>Ve.email==="ada@example.com")?.tenants.length===2,"the same identity across tenants groups together"),e(De.find((Ve)=>Ve.email==="cus@example.com")?.tenants.length===1,"a distinct identity gets its own group")}},{name:"max caps session entries and cookiesPrefix scopes a shared store",fn:async()=>{let Ne=new Hn,Le=await Yn(Ne),ze=()=>new Request("https://app.ecv.local/",{headers:{cookie:Le}}),De=await ln(ze(),{sessionStore:Ne,max:2});e(De.tenants.map((Je)=>Je.tenantId).join(",")==="alpha,beta","max keeps the newest session windows");let Ve=await ln(ze(),{sessionStore:Ne,cookiesPrefix:"ion.sso.beta"});e(Ve.tenants.length===1&&Ve.tenants[0]?.tenantId==="beta","cookiesPrefix surfaces only this app’s sessions on a shared store")}},{name:"discovered hints append after sessions, dedupe live tenants, and ignore expired hints",fn:async()=>{let Ne=new Hn,Le=await Yn(Ne),ze=(Ge,We,Ke)=>`ion.discovered.${Ge}.profile=${btoa(JSON.stringify({v:1,kind:"discovered",tenantId:Ge,tenantName:Ke,authType:"sso",expiresAt:We.toISOString(),user:{name:"Ada",email:"ada@example.com"}}))}`,De=[Le,ze("delta",new Date(Date.now()+3600000),"Delta"),ze("alpha",new Date(Date.now()+3600000),"Duplicate Alpha"),ze("expired",new Date(Date.now()-1000),"Expired")].join("; "),{tenants:Ve}=await ln(new Request("https://app.ecv.local/",{headers:{cookie:De}}),{sessionStore:Ne});e(Ve.map((Ge)=>Ge.tenantId).join(",")==="alpha,beta,gamma,delta",`discovered hints append after sessions, got ${Ve.map((Ge)=>Ge.tenantId).join(",")}`);let Je=Ve.at(-1);e(Je?.source==="discovered","session-less hint carries discovered source"),e(Je?.tenantName==="Delta","discovered hint carries its tenant label")}},{name:"owner-grant tenants merge for a root customer without duplicating live sessions",fn:async()=>{let Ne=new Hn,Le=await Yn(Ne);await Ne.create({sid:"sid-root",sub:"cust-root",authType:"ciam",createdAt:new Date,expiresAt:new Date(Date.now()+3600000),claims:{sub:"cust-root",name:"Root Owner",email:"owner@example.com"}});let ze=qu(Du({id:"ecv-session-entries",ciam:{cookiesSecure:!1}}),{validators:zu(),stores:{sessionStore:Ne,customerStore:new Uu,catalog:ju({permissions:[Nu()]}),...Ou()}});try{await ze.ready(1000);for(let Je of["alpha","delta"])await $u(ze,{tenantId:Je,displayName:Je}),await ze.authz.grant({subject:"cust-root",subjectType:"customer",targetType:"group",target:Mu(Je),resources:[Lu(Je)]});let{tenants:De}=await ln(new Request("https://app.ecv.local/",{headers:{cookie:`${Le}; ion.ciam.session=sid-root`}}),{sessionStore:Ne,ion:ze});e(De.map((Je)=>Je.tenantId).join(",")==="alpha,beta,gamma,delta",`owner tenants append after sessions without duplicates, got ${De.map((Je)=>Je.tenantId).join(",")}`);let Ve=De.find((Je)=>Je.tenantId==="delta");e(Ve?.source==="owner","an owner-grant entry carries source owner"),e(Ve?.user.email==="owner@example.com","owner entries project the root customer identity"),e(De.find((Je)=>Je.tenantId==="alpha")?.source==="session","a live session wins over an owner grant for the same tenant")}finally{ze.close()}}}]}}async function be(){let Be=performance.now(),{tests:Ne}=T(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Session entries",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{ACCEPT_JSON as ba,CONTENT_TYPE_FORM as Vu,readJsonResponse as Bu}from"@ionite/core";import{completeLoginFlow as Hi,runLoginToCallback as Yi}from"@ionite/devtools";import{completeLoginFlow as V,runLoginToCallback as Se}from"@ionite/devtools";var Xi={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 Zi(Be,Ne){return s("SSO Login Endpoint",async()=>{let Le=Be.loginPath??"/api/auth/login",ze=await Ne(Le);e(ze.status===302,`Expected 302 redirect, got ${ze.status}`);let De=ze.headers.get("Location");if(e(De!==null,"Missing Location header in redirect"),Be.expectedAuthorizationUrlPattern)e(Be.expectedAuthorizationUrlPattern.test(De),`Authorization URL does not match expected pattern: ${De}`);let Ve=new URL(De);e(Ve.searchParams.has("client_id"),"Missing client_id in authorization URL"),e(Ve.searchParams.has("redirect_uri"),"Missing redirect_uri in authorization URL"),e(Ve.searchParams.has("response_type"),"Missing response_type in authorization URL"),e(Ve.searchParams.has("scope"),"Missing scope in authorization URL"),e(Ve.searchParams.has("state"),"Missing state in authorization URL"),e(Ve.searchParams.has("code_challenge"),"Missing code_challenge (PKCE) in authorization URL"),e(Ve.searchParams.has("code_challenge_method"),"Missing code_challenge_method (PKCE) in authorization URL");let Je=qs(ze.headers),Ge=Array.from(Je.keys()).some((We)=>We.includes(".state"));return e(Ge,"Missing state cookie in response"),{details:{authorizationUrl:De,pkceEnabled:!0,stateParameter:Ve.searchParams.get("state")}}})}async function Qi(Be,Ne){return s("SSO User Endpoint (Unauthenticated)",async()=>{let Le=Be.userPath??"/api/auth/user",ze=await Ne(Le,{headers:ba});return e(ze.status===401,`Expected 401 Unauthorized, got ${ze.status}`),{details:{status:ze.status}}})}async function ec(Be,Ne){return s("SSO Logout Endpoint",async()=>{let Le=Be.logoutPath??"/api/auth/logout",ze=await Ne(`${Le}?redirect=/`);e(ze.status===302||ze.status===200,`Expected 302 or 200, got ${ze.status}`);let De=qs(ze.headers),Ve=[];for(let[We,Ke]of De.entries())if(Ke===""||Ke.includes("Max-Age=0"))Ve.push(We);let Je=ze.headers.getSetCookie?.()??[],Ge=Je.some((We)=>We.includes("Max-Age=0")||We.includes("expires=Thu, 01 Jan 1970"));return e(Ge||Ve.length>0||Je.length===0,"Logout should clear session cookies"),{details:{status:ze.status,clearedCookies:Ve}}})}async function tc(Be,Ne){return s("SSO Back-Channel Logout Endpoint",async()=>{let Le=Be.backChannelLogoutPath??"/api/auth/logout/backchannel",ze=await Ne(Le,{method:"POST",headers:{...ba,...Vu},body:""});return e(ze.status===400||ze.status===200||ze.status===404,`Expected 400 or 200, got ${ze.status}`),{details:{status:ze.status,endpointExists:ze.status!==404}}})}async function sc(Be,Ne){return s("SSO Callback Endpoint (Invalid)",async()=>{let Le=Be.callbackPath??"/api/auth/callback",ze=await Ne(`${Le}?code=invalid&state=invalid`);e(ze.status===302,`Expected redirect for invalid callback, got ${ze.status}`);let De=ze.headers.get("Location")??"";if(!De.startsWith("/")){let Ve=new URL(`${n(Be)}/`).origin;e(new URL(De).origin===Ve,`Failure redirect must stay same-origin: ${De}`)}return{details:{status:ze.status,location:De,handlesInvalidCallback:!0}}})}async function Ju(Be,Ne,Le){return s("SSO Callback Error Surfaced",async()=>{let ze=Be.callbackPath??"/api/auth/callback",De=await Ne(`${ze}?code=invalid&state=invalid`);e(De.status===302,`Expected redirect for invalid callback, got ${De.status}`);let Ve=De.headers.get("Location")??"";return e(Le.test(Ve),`Failure redirect does not surface an error code (expected ${Le}): ${Ve}`),{details:{status:De.status,location:Ve}}})}async function ac(Be,Ne){if(!Be.tokenPath)return c("SSO Token Endpoint (Unauthenticated)","Token path not configured");let Le=Be.tokenPath;return s("SSO Token Endpoint (Unauthenticated)",async()=>{let ze=await Ne(Le,{headers:ba});return e(ze.status===401||ze.status===404,`Expected 401 Unauthorized or 404, got ${ze.status}`),{details:{status:ze.status}}})}async function nc(Be,Ne){if(!Be.refreshPath)return c("SSO Refresh Endpoint (Unauthenticated)","Refresh path not configured");let Le=Be.refreshPath;return s("SSO Refresh Endpoint (Unauthenticated)",async()=>{let ze=await Ne(Le,{headers:ba});return e(ze.status===401||ze.status===404,`Expected 401 Unauthorized or 404, got ${ze.status}`),{details:{status:ze.status}}})}function Ki(Be){return Array.from(Be.keys()).filter((Ne)=>Ne.includes(".state"))}function Fi(Be){return Be.endsWith(".access")||Be.endsWith(".id")||Be.endsWith(".refresh")||Be.endsWith(".control")}function Gu(Be){for(let Ne of Be.keys()){let Le=Ne.indexOf(".state");if(Le>0)return Ne.slice(0,Le);if(Ne.endsWith(".session"))return Ne.slice(0,-8)}return}function Wu(Be,Ne){return(Be.getSetCookie?.()??[]).some((ze)=>{let De=ze.split("=")[0]?.trim()??"";if(!Ne(De))return!1;return ze.includes("Max-Age=0")||/expires=Thu, 01 Jan 1970/i.test(ze)})}async function oc(Be,Ne){return s("SSO State Cookie Multi-Tab Isolation",async()=>{let Le=Be.loginPath??"/api/auth/login",[ze,De]=await Promise.all([Ne(Le),Ne(Le)]);e(ze.status===302&&De.status===302,"Both login attempts should redirect");let Ve=new URL(ze.headers.get("Location")??"").searchParams.get("state"),Je=new URL(De.headers.get("Location")??"").searchParams.get("state");e(!!Ve&&!!Je&&Ve!==Je,"Each login attempt must mint a distinct state");let Ge=Ki(qs(ze.headers)),We=Ki(qs(De.headers));e(Ge.length===1&&We.length===1,"Each login should set exactly one state cookie"),e(Ge[0]!==We[0],"Concurrent logins must use isolated (distinct-name) state cookies"),e(Ge[0].includes(Ve)&&We[0].includes(Je),"The state value must be encoded in the state cookie name");let Ke=(ze.headers.getSetCookie?.()??[]).find((Fe)=>Fe.split("=")[0]?.includes(".state"));return e(!!Ke,"Login must emit a Set-Cookie for the state cookie"),e(/SameSite=Lax/i.test(Ke),`State cookie must be SameSite=Lax for the cross-site IdP callback, got: ${Ke}`),{details:{state1:Ve,state2:Je,cookie1:Ge[0],cookie2:We[0],stateSetCookie:Ke}}})}async function rc(Be,Ne){return s("SSO State Cookie Cleared On Error",async()=>{let Le;try{Le=await Yi(Be,Ne,{bogusCode:!0})}catch(Ve){return{details:{skipped:!0,reason:Ve instanceof Error?Ve.message:String(Ve)}}}let{callback:ze,state:De}=Le;return e(ze.status===302,`Failed callback should redirect, got ${ze.status}`),e(Wu(ze.headers,(Ve)=>Ve.includes(".state")&&(De?Ve.includes(De):!0)),"Callback error path must clear the per-attempt state cookie"),{details:{status:ze.status,state:De}}})}async function ic(Be,Ne){return s("SSO Open-Redirect Denied (same-origin floor)",async()=>{let Le;try{Le=await Yi(Be,Ne,{redirectParam:"https://evil.example/pwned"})}catch(Ve){return{details:{skipped:!0,reason:Ve instanceof Error?Ve.message:String(Ve)}}}let{callback:ze}=Le;e(ze.status===302,`Successful callback should redirect, got ${ze.status}`);let De=ze.headers.get("Location")??"";if(e(!De.includes("evil.example"),`Open redirect leaked to external origin: ${De}`),!De.startsWith("/")){let Ve=new URL(`${n(Be)}/`).origin;e(new URL(De).origin===Ve,`Post-login redirect must stay same-origin: ${De}`)}return{details:{location:De}}})}async function cc(Be,Ne){return s("SSO Opaque Session Cookie Only",async()=>{let{cookies:Le,success:ze,error:De}=await Hi(Be,Ne);e(ze,De||"Login flow failed");let Ve=Array.from(Le.keys());e(Ve.some((Fe)=>Fe.includes(".session")),"Opaque login must set a `.session` cookie");let Je=Ve.find(Fi);e(!Je,`Opaque-only login must not set token cookies, found: ${Je}`);let Ge=Be.userPath;if(!Ge)throw Error("User path not configured");let We=await Ne(Ge,{headers:{...ba,Cookie:oa(Le)}});e(We.status===200,`User endpoint returned ${We.status} after login, expected 200`);let Ke=Array.from(qs(We.headers).keys()).find(Fi);return e(!Ke,`Session resolution must not set token cookies, found: ${Ke}`),{details:{sessionCookieOnly:!0}}})}async function dc(Be,Ne){return s("SSO Legacy Token Cookies Rejected",async()=>{let Le=Be.loginPath??"/api/auth/login",ze=Be.userPath??"/api/auth/user",De=await Ne(Le);e(De.status===302,`Login should redirect, got ${De.status}`);let Ve=Gu(qs(De.headers));if(!Ve)return{details:{skipped:!0,reason:"Could not derive cookie prefix from login response"}};let Je=new Map([[`${Ve}.access`,"legacy-access"],[`${Ve}.id`,"legacy-id"],[`${Ve}.refresh`,"legacy-refresh"],[`${Ve}.control`,btoa(JSON.stringify({token_type:"Bearer",expires_in:3600}))]]),Ge=await Ne(ze,{headers:{...ba,Cookie:oa(Je)}});return e(Ge.status===401,`Legacy token cookies must not authenticate (expected 401), got ${Ge.status}`),{details:{status:Ge.status,prefix:Ve}}})}async function lc(Be,Ne){return s("SSO JIT User Provisioning Flow",async()=>{let{cookies:Le,success:ze,error:De}=await Hi(Be,Ne);e(ze,De||"Login flow failed");let Ve=Array.from(Le.keys()).some((Ke)=>Ke.includes(".session"));e(Ve,"No opaque session cookie set after login");let Je=Be.userPath;if(!Je)throw Error("User path not configured");let Ge=await Ne(Je,{headers:{...ba,Cookie:oa(Le)}});e(Ge.status===200,`User endpoint returned ${Ge.status} after login, expected 200`);let We=await Bu(Ge);return e(We.id!==void 0,"User data missing id field"),Ku(Be,We),{details:{userId:We.id,userName:We.userName,name:We.name,email:We.email,avatar:We.avatar,jitFlowCompleted:!0}}})}function Ku(Be,Ne){let Le=Be.expectedUser;if(!Le)return;if(Le.id!==void 0)e(Ne.id===Le.id,`Expected user id ${Le.id}, got ${String(Ne.id)}`);if(Le.userName!==void 0)e(Ne.userName===Le.userName,`Expected username ${Le.userName}, got ${String(Ne.userName)}`);if(Le.name!==void 0)e(Ne.name===Le.name,`Expected name ${Le.name}, got ${String(Ne.name)}`);if(Le.email!==void 0)e(Ne.email===Le.email,`Expected email ${Le.email}, got ${String(Ne.email)}`);if(Le.avatar!==void 0)e(Ne.avatar===Le.avatar,`Expected avatar ${Le.avatar}, got ${String(Ne.avatar)}`);if(Le.requireSso)e(Ne.sso!==void 0,"Expected SSO auth envelope to be preserved on user response")}async function ke(Be){let Ne=performance.now(),Le={...Xi,...Be},ze=a(Le),De=[];De.push(await Zi(Le,ze)),De.push(await Qi(Le,ze)),De.push(await ec(Le,ze)),De.push(await tc(Le,ze)),De.push(await sc(Le,ze)),De.push(await ac(Le,ze)),De.push(await nc(Le,ze)),De.push(await oc(Le,ze)),De.push(await rc(Le,ze)),De.push(await ic(Le,ze)),De.push(await dc(Le,ze)),De.push(await lc(Le,ze)),De.push(await cc(Le,ze));let Ve=performance.now()-Ne,Je=De.filter((Ke)=>Ke.passed).length,Ge=De.filter((Ke)=>!Ke.passed).length,We=De.filter((Ke)=>Ke.details?.skipped).length;return{suite:"SSO",passed:Ge===0,tests:De,duration:Ve,summary:{total:De.length,passed:Je-We,failed:Ge,skipped:We}}}function C(Be){let Ne={...Xi,...Be},Le=a(Ne);return{tests:[{name:"login endpoint redirects to authorization URL",fn:async()=>{let Ve=await Zi(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"user endpoint returns 401 when unauthenticated",fn:async()=>{let Ve=await Qi(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"callback endpoint redirects on invalid requests",fn:async()=>{let Ve=await sc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"token endpoint returns 401 when unauthenticated",fn:async()=>{let Ve=await ac(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"refresh endpoint returns 401 when unauthenticated",fn:async()=>{let Ve=await nc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"state cookie is isolated per login attempt (multi-tab)",fn:async()=>{let Ve=await oc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"state cookie is cleared on callback error",fn:async()=>{let Ve=await rc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"cross-origin post-login redirect is floored to same-origin",fn:async()=>{let Ve=await ic(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"legacy token cookies do not authenticate (opaque-only)",fn:async()=>{let Ve=await dc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}],ext:{createJITTests:()=>[{name:"user provisioning flow completes successfully",fn:async()=>{let Ve=await lc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"login sets only an opaque session cookie (no token cookies)",fn:async()=>{let Ve=await cc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}],createLogoutTests:()=>[{name:"logout endpoint clears cookies",fn:async()=>{let Ve=await ec(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}],createBackChannelLogoutTests:()=>[{name:"endpoint exists",fn:async()=>{let Ve=await tc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}}],createCallbackErrorSurfacedTests:(Ve)=>[{name:"callback error surfaced",fn:async()=>{let Je=await Ju(Ne,Le,Ve);if(!Je.passed)throw Error(Je.error)}}]}}}import{CONTENT_TYPE_JSON as un,ION_TENANT_MANAGE as Fu}from"@ionite/core";import{InMemoryTenantStore as pn,InMemoryWorkloadTokenStore as uc,ionite as qa,isRequestTokenExpired as Xn,validateWorkloadToken as Hu}from"@ionite/server";import{createValidators as Zn}from"@ionite/valibot";function $a(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function pc(){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:[Fu.id]}]},tm:{enabled:!0,ionite:{id:"eci",url:"http://eci.example/ionite",issuer:"http://idp.example/api/t/master",jwksUri:"http://idp.example/jwks",audience:"eci",clientId:"eci"}}}}async function mc(){let Be=await crypto.subtle.generateKey({name:"RSASSA-PKCS1-v1_5",modulusLength:2048,publicExponent:new Uint8Array([1,0,1]),hash:"SHA-256"},!0,["sign","verify"]),Ne=`ecv-tm-${crypto.randomUUID()}`,Le=await crypto.subtle.exportKey("jwk",Be.publicKey),ze=JSON.stringify({keys:[{...Le,kid:Ne,alg:"RS256",use:"sig"}]}),De=(Ve)=>{let Je=typeof Ve==="string"?new TextEncoder().encode(Ve):new Uint8Array(Ve),Ge="";for(let We of Je)Ge+=String.fromCharCode(We);return btoa(Ge).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/g,"")};return{jwksUri:`data:application/json,${encodeURIComponent(ze)}`,async sign(Ve){let Je=De(JSON.stringify({alg:"RS256",kid:Ne,typ:"JWT"})),Ge=De(JSON.stringify(Ve)),We=`${Je}.${Ge}`,Ke=await crypto.subtle.sign({name:"RSASSA-PKCS1-v1_5"},Be.privateKey,new TextEncoder().encode(We));return`${We}.${De(Ke)}`}}}function hc(Be={}){let Ne=Math.floor(Date.now()/1000);return{iss:"https://ionite.example",aud:"ecv-app",azp:"ecv-ionite",sub:"ecv-ionite",iat:Ne,exp:Ne+300,jti:crypto.randomUUID(),ioniteName:"ECV ionite",ioniteId:"ecv-console",productId:"ecv-product",applicationId:"ecv-application",...Be}}function R(){return{tests:[{name:"SSO-shaped ionite TM rejects tenant create without a workload token",fn:async()=>{let Ne=Zn(),Le=new pn,ze=qa($a(pc()),{validators:Ne,stores:{workloadTokenStore:new uc},tm:{basePath:"/api/tenants",store:Le,validators:Ne,onConnectRequest:async()=>Response.json({error:"not implemented"},{status:501})}});await ze.ready();let De=ze.tm;if(!De)throw Error("ion.tm must be available when wired");let Ve=await De.handler(new Request("http://sso.example/api/tenants",{method:"POST",headers:un,body:JSON.stringify({id:"acme",companyId:"acme",companyName:"Acme",environmentType:"DEV"})}));e(Ve.status===401,`expected 401 without workload auth, got ${Ve.status}`),await ze.close()}},{name:"ion.tm signed consent, redemption, and config export are bound and single-use",fn:async()=>{let Ne=await mc(),Le=Zn(),ze=!1,De=new pn({ionCache:{fetch:async()=>({isReady:()=>ze}),delete:()=>{return}}}),Ve={calls:0},Je=()=>Ve.calls,Ge={id:"tm-contract",tm:{enabled:!0,ionite:{id:"ecv-ionite",name:"ECV ionite",url:"https://ionite.example",issuer:"https://ionite.example",jwksUri:Ne.jwksUri,audience:"ecv-app",clientId:"ecv-ionite"},maxRedeemAttempts:3}},We=qa($a(Ge),{validators:Le,tm:{basePath:"/api/tenants",store:De,validators:Le,onConnectRequest:async({staged:Ft})=>Response.json({ioniteName:Ft.ioniteName,ioniteId:Ft.ioniteId}),exportConfig:async()=>({version:1,config:{id:"tm-exported"}}),onHandoverApplied:async({destinationLocator:Ft})=>{Ve.destination=Ft,Ve.calls++}}});await We.ready();let Ke=We.tm;if(!Ke)throw Error("ion.tm must be available");e(Ke.isReady?.()===!0,"ion.tm must be ready");let Fe=await Ne.sign(hc({jti:void 0})),He=(Ft,Os)=>Ke.handler(new Request("https://app.example/api/tenants",{method:"POST",headers:{Authorization:`Bearer ${Fe}`,...un},body:JSON.stringify({id:Ft,ioniteId:Os,companyId:"tm-company",companyName:"TM Tenant",environmentType:"DEV"})}));e((await He("tm-created","ionite-created")).status===201,"initial TM create failed"),e((await He("different-suggestion","ionite-created")).status===201,"TM retry was not idempotent by ionite id"),e((await De.getByIoniteId("ionite-created"))?.id==="tm-created","TM retry changed the App tenant id");let Xe=await He("tm-created","different-ionite-id");e(Xe.status===409,`App id collision returned ${Xe.status}`),e((await Xe.json()).error==="tenant_id_taken","App id collision omitted its stable error code");let Ye=new Date;await De.upsert({id:"tm-tenant",companyId:"tm-company",companyName:"TM Tenant",environmentType:"DEV",status:"completed",createdAt:Ye,updatedAt:Ye});let Ze=await Ne.sign(hc({tenantHint:"tm-tenant"})),tt=await Ke.handler(new Request(`https://app.example/api/tenants/integrations/authorize?handle=${Ze}`));e(tt.status===200,`expected authorize 200, got ${tt.status}`);let Qe=await tt.text();e(!Qe.includes("handleHash"),"browser response exposed handleHash"),e(!Qe.includes("exportTokenHash"),"browser response exposed export authorization");let st=await De.get("tm-tenant");e(st?.integration?.status==="consent_requested","authorize did not stage consent_requested"),e(st?.ioniteId===st?.integration?.ioniteId,"authorize did not bind the top-level ionite id"),e(!st?.configLocator,"authorize changed configLocator");let et=(Ft)=>Ke.handler(new Request("https://app.example/api/tenants/integrations/redeem",{method:"POST",headers:{...un,...Ft?{Authorization:`Bearer ${Ft}`}:{}},body:JSON.stringify({handle:Ze})}));e((await et()).status===401,"redeem accepted a request without workload authentication"),e((await et(Fe)).status===202,"pending consent did not return 202"),await Ke.recordConsent({handle:Ze,tenantId:"tm-tenant",decision:"declined",decidedBy:"owner:ecv"});let nt=await et(Fe);e(nt.status===200,`declined consent returned ${nt.status}`),e((await nt.json()).status==="declined","declined consent was not resumable"),await Ke.recordConsent({handle:Ze,tenantId:"tm-tenant",decision:"consented",decidedBy:"owner:ecv"});let dt=await et(Fe);e(dt.status===200,`consented redemption returned ${dt.status}`);let pt=await dt.json();e(pt.status==="consented","redemption did not report consented"),e(typeof pt.authorization==="string","redemption omitted authorization"),e((await et(Fe)).status===410,"consent redemption was not single-use");let ct=(Ft)=>Ke.handler(new Request("https://app.example/api/tenants/integrations/config-export",{method:"POST",headers:{Authorization:`Bearer ${Fe}`,...un},body:JSON.stringify({handle:Ze,authorization:Ft})})),ft=await ct(pt.authorization);e(ft.status===200,`config export returned ${ft.status}`),e((await ft.json()).config.id==="tm-exported","config export returned the wrong envelope"),e((await ct(pt.authorization)).status===401,"config export was not single-use"),e(!(await De.get("tm-tenant"))?.configLocator,"consent or export changed configLocator");let ut=await Ke.handler(new Request("https://app.example/api/tenants/tm-tenant",{method:"PUT",headers:{Authorization:`Bearer ${Fe}`,...un},body:JSON.stringify({configLocator:{type:"vault",vaultPath:"/tenant/tm-tenant"}})}));e(ut.status===201,`locator attach returned ${ut.status}`),e(Je()===0,"locator persistence called cleanup before destination config applied");let Rt=await Ke.handler(new Request("https://app.example/api/tenants/tm-tenant",{headers:{Authorization:`Bearer ${Fe}`}}));e((await Rt.json()).integration.status==="consented","unready destination appeared integrated"),ze=!0;let jt=await Ke.handler(new Request("https://app.example/api/tenants/tm-tenant",{headers:{Authorization:`Bearer ${Fe}`}}));e((await jt.json()).integration.status==="consented","wire status must remain consented"),e(Je()===1,"ready destination did not call cleanup exactly once"),e(Ve.destination?.type==="vault"&&Ve.destination.vaultPath==="/tenant/tm-tenant","handover hook received the wrong destination locator"),await Ke.handler(new Request("https://app.example/api/tenants/tm-tenant",{headers:{Authorization:`Bearer ${Fe}`}})),e(Je()===1,"completed handover called cleanup more than once"),await We.close()}},{name:"ion.tm.actionRequired uses the RemoteConfig request-token TTL and defaults to one hour",fn:async()=>{let Ne=await mc(),Le=Zn(),ze={id:"ecv-ionite",url:"https://ionite.example",issuer:"https://ionite.example",jwksUri:Ne.jwksUri,audience:"ecv-app",clientId:"ecv-ionite"},De=qa($a({id:"tm-ttl-invalid",tm:{enabled:!0,ionite:ze,requestTokenTtlSeconds:30}}),{validators:Le,tm:{basePath:"/api/tenants",store:new pn,validators:Le}}),Ve=!0;try{await De.ready(1000)}catch{Ve=!1}e(Ve===!1,"requestTokenTtlSeconds below 60 must reject config apply"),await De.close();let Je=qa($a({id:"tm-ttl-configured",tm:{enabled:!0,ionite:ze,requestTokenTtlSeconds:120}}),{validators:Le,tm:{basePath:"/api/tenants",store:new pn,validators:Le}});await Je.ready(1000);let Ge=Date.now(),We=Je.tm?.actionRequired({path:"/onboard",origin:"https://app.example"});if(!We)throw Error("ion.tm.actionRequired must be available");let Ke=new URL(We.actionUrl);e(Ke.origin==="https://app.example"&&Ke.pathname==="/onboard","action URL uses the App path"),e(Ke.searchParams.get("rt")===We.requestToken,"action URL carries the request token"),e(We.requestToken.length>=32,"request token is a high-entropy value");let Fe=Date.parse(We.expiresAt)-Ge;e(Fe>=119000&&Fe<=121000,"configured TTL is two minutes"),e(Je.tm?.describe().requestTokenTtlSeconds===120,"describe reports the effective TTL"),await Je.close();let He=qa($a({id:"tm-ttl-default",tm:{enabled:!0,ionite:ze}}),{validators:Le,tm:{basePath:"/api/tenants",store:new pn,validators:Le}});await He.ready(1000),e(He.tm?.describe().requestTokenTtlSeconds===3600,"omitted TTL defaults to one hour"),e(Xn(void 0),"a missing expiry is expired"),e(Xn("not-a-date"),"an unparseable expiry is expired"),e(Xn(new Date(Date.now()-1000).toISOString()),"a past expiry is expired"),e(!Xn(new Date(Date.now()+60000).toISOString()),"a future expiry is valid"),await He.close()}},{name:"SSO-shaped client registration rejects a request with no workload token",fn:async()=>{let Ne=qa($a(pc()),{validators:Zn(),stores:{workloadTokenStore:new uc}});await Ne.ready();let Le=await Hu(new Request("http://sso.example/api/admin/tenants/acme/clients",{method:"POST"}),Ne);e(Le.valid===!1,"missing bearer must not validate"),await Ne.close()}}]}}async function Te(){return R()}import{decodeDisplayCookie as mn,defaultLogger as zs,discoveredProfileCookieName as gc,profileCookieName as Ds}from"@ionite/core";import{ciam as Qn,planDiscoveredTenantCookies as fc,planDisplayCookieHeaders as Ma,signCompactJwt as Yu,sso as Na,staticSigningKeyRing as ur,verifyCompactJwt as ka}from"@ionite/core/server";import{InMemorySessionStore as Vs,InMemoryUserStore as Xu}from"@ionite/server";import{createValidators as eo}from"@ionite/valibot";var Ht="ion.sso.acme",hn="ion.ciam.acme",La={path:"/",secure:!1,sameSite:"Lax"};async function to(Be,Ne){let Le=new TextEncoder,ze=(We)=>btoa(String.fromCharCode(...We)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/g,""),De=(We)=>ze(Le.encode(JSON.stringify(We))),Ve=`${De({alg:"HS256",kid:Ne,typ:"JWT"})}.${De(Be)}`,Je=await crypto.subtle.importKey("raw",Le.encode("next-high-entropy-selector-secret"),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),Ge=new Uint8Array(await crypto.subtle.sign("HMAC",Je,Le.encode(Ve)));return`${Ve}.${ze(Ge)}`}function za(Be,Ne){let Le=Be.find((De)=>De.startsWith(`${Ne}=`)&&!De.includes("Max-Age=0"));if(!Le)return;let ze=Le.indexOf("=");return Le.slice(ze+1,Le.indexOf(";"))}function x(){return{tests:[{name:"display cookies stay off and clear on touch unless Ops enables them",fn:async()=>{let Ne=Ds(Ht),Le=await Ma({cookieHeader:`${Ne}=c3RhbGU`,sessionStore:new Vs,cookiesPrefix:Ht,authType:"sso",expiresAt:new Date(Date.now()+60000),user:{name:"Ada"},displayCookie:{enabled:!1},attr:{...La,domain:"app.ecv.local"}});e(Le.length===1,"disabled flag must clear an existing display cookie"),e(Le[0]?.includes("Max-Age=0"),"clear uses Max-Age=0"),e(Le[0]?.includes("Domain=app.ecv.local"),"clear carries the configured cookie Domain"),e(!Le[0]?.includes("HttpOnly"),"display cookie clear is not an auth cookie")}},{name:"an enabled display cookie carries the projection and no auth material",fn:async()=>{let Ne=new Vs,Le=await Ma({cookieHeader:null,sessionStore:Ne,cookiesPrefix:Ht,authType:"sso",tenantId:"acme",expiresAt:new Date(Date.now()+60000),user:{name:"Ada Lovelace",email:"ada@example.com",avatar:"https://example.com/a.png",sid:"secret-sid",sub:"user-1",tokens:"nope"},displayCookie:{enabled:!0,max:10},attr:La}),ze=za(Le,Ds(Ht));e(!!ze,"enabled flag must set a profile cookie");let De=mn(ze);e(De?.tenantId==="acme","envelope keeps the tenant id"),e(De?.authType==="sso","envelope keeps the auth type"),e(De?.user.name==="Ada Lovelace","default projection keeps name"),e(De?.user.email==="ada@example.com","default projection keeps email"),e(De?.user.sid===void 0,"sid must not be in the display cookie"),e(De?.user.sub===void 0,"sub must not be in the display cookie"),e(De?.user.tokens===void 0,"tokens must not be in the display cookie"),e(!Le.some((Ve)=>Ve.includes("HttpOnly")),"display cookie is readable by the page")}},{name:"a custom projection replaces the default and still cannot smuggle auth fields",fn:async()=>{let Ne=await Ma({cookieHeader:null,sessionStore:new Vs,cookiesPrefix:Ht,authType:"sso",expiresAt:new Date(Date.now()+60000),user:{name:"Ada",title:"Engineer",sid:"no"},projector:(ze)=>({title:ze.title,sid:"no"}),displayCookie:{enabled:!0},attr:La}),Le=mn(za(Ne,Ds(Ht)));e(Le?.user.title==="Engineer","custom projection is honored"),e(Le?.user.name===void 0,"default fields are not added when a projection returns"),e(Le?.user.sid===void 0,"projection cannot add sid")}},{name:"cap keeps the newest sessions and evicts orphans without another tenant runtime",fn:async()=>{let Ne=new Vs,Le=new Date(Date.now()+30000),ze=new Date(Date.now()+120000);await Ne.create({sid:"live-sid",sub:"user",authType:"sso",tenantId:"other",createdAt:new Date,expiresAt:ze});let De="ion.sso.other.profile",Ve=btoa(JSON.stringify({v:1,tenantId:"other",authType:"sso",expiresAt:ze.toISOString(),user:{name:"Other"}})),Je="ion.sso.gone.profile",Ge=btoa(JSON.stringify({v:1,tenantId:"gone",authType:"sso",expiresAt:Le.toISOString(),user:{name:"Gone"}}));await Ne.create({sid:"forged-sid",sub:"user",authType:"sso",tenantId:"forged",createdAt:new Date});let We="ion.sso.forged.profile",Ke=btoa(JSON.stringify({v:1,tenantId:"forged",authType:"sso",expiresAt:"not-a-date",user:{name:"Forged"}})),Fe=await Ma({cookieHeader:`${De}=${Ve}; ion.sso.other.session=live-sid; ${Je}=${Ge}; ${We}=${Ke}; ion.sso.forged.session=forged-sid`,sessionStore:Ne,cookiesPrefix:Ht,authType:"sso",tenantId:"acme",expiresAt:Le,user:{name:"Ada"},displayCookie:{enabled:!0,max:1},attr:La});e(!!za(Fe,Ds(Ht)),"the cookie being written is kept"),e(Fe.some((He)=>He.startsWith(`${De}=`)&&He.includes("Max-Age=0")),"a live peer beyond max is evicted"),e(Fe.some((He)=>He.startsWith(`${Je}=`)&&He.includes("Max-Age=0")),"a profile cookie with no live peer session is evicted"),e(Fe.some((He)=>He.startsWith(`${We}=`)&&He.includes("Max-Age=0")),"a forged envelope expiry sorts last and is evicted beyond the cap")}},{name:"userEndpoint off is 404, display returns the projection, full returns the user",fn:async()=>{let Ne=new Vs,Le=new Date(Date.now()+3600000);await Ne.create({sid:"sid-user",sub:"user-1",authType:"sso",tenantId:"acme",createdAt:new Date,expiresAt:Le,claims:{sub:"user-1",iss:"https://idp.ecv.local",aud:"ecv",name:"Ada",email:"ada@example.com",preferred_username:"ada"},tokens:{access_token:"access",id_token:"id",token_type:"Bearer",expires:Le.toISOString(),scope:"openid"}});let ze=eo(),De={sessionStore:Ne,cookiesPrefix:Ht,cookiesSecure:!1,userUrl:"/auth/user",tenantId:"acme"},Ve=new Request("https://app.ecv.local/auth/user",{headers:{cookie:`${Ht}.session=sid-user`}}),Ge=await Na(ze.sso,zs,void 0,{...De,userEndpoint:"off"})?.handler(Ve);e(Ge?.status===404,`off must be 404, got ${Ge?.status}`);let Ke=await Na(ze.sso,zs,void 0,{...De,userEndpoint:"display",displayCookie:{enabled:!1},displayUser:()=>({name:"Projected"})})?.handler(Ve);e(Ke?.status===200,`display must be 200, got ${Ke?.status}`);let Fe=await Ke?.json();e(Fe.name==="Projected","display policy returns the projection"),e(Fe.email===void 0,"display policy does not return the full user");let Xe=await Na(ze.sso,zs,void 0,{...De,userEndpoint:"full"})?.handler(Ve);e(Xe?.status===200,`full must be 200, got ${Xe?.status}`);let Ye=await Xe?.json();e(Ye.email==="ada@example.com","full policy returns the verified user");let Ze=Xe?.headers.getSetCookie()??[];e(!Ze.some((tt)=>tt.startsWith(Ds(Ht))),"a disabled display cookie is not written")}},{name:"the user endpoint heals a missing cookie, skips a present one, and clears after Ops disables",fn:async()=>{let Ne=new Vs,Le=new Date(Date.now()+3600000);await Ne.create({sid:"sid-user",sub:"user-1",authType:"sso",tenantId:"acme",createdAt:new Date,expiresAt:Le,claims:{sub:"user-1",iss:"https://idp.ecv.local",aud:"ecv",name:"Ada",email:"ada@example.com"},tokens:{access_token:"access",id_token:"id",token_type:"Bearer",expires:Le.toISOString(),scope:"openid"}});let ze=eo(),De={sessionStore:Ne,cookiesPrefix:Ht,cookiesSecure:!1,userUrl:"/auth/user",tenantId:"acme"},Ve=Ds(Ht),Je=Na(ze.sso,zs,void 0,{...De,displayCookie:{enabled:!0}}),Ge=await Je?.handler(new Request("https://app.ecv.local/auth/user",{headers:{cookie:`${Ht}.session=sid-user`}}));e(Ge?.status===200,`heal request must be 200, got ${Ge?.status}`);let We=za(Ge?.headers.getSetCookie()??[],Ve);e(!!We,"a missing profile cookie is healed on the authenticated read path");let Ke=mn(We);e(Ke?.tenantId==="acme","healed envelope carries the tenant id"),e(Ke?.user.email==="ada@example.com","healed envelope carries the projection");let Fe=await Je?.handler(new Request("https://app.ecv.local/auth/user",{headers:{cookie:`${Ht}.session=sid-user; ${Ve}=${We}`}}));e(Fe?.status===200,`present request must be 200, got ${Fe?.status}`),e(!(Fe?.headers.getSetCookie()??[]).some((Ye)=>Ye.startsWith(Ve)),"a present profile cookie is not rewritten per request");let Xe=await Na(ze.sso,zs,void 0,{...De,displayCookie:{enabled:!1}})?.handler(new Request("https://app.ecv.local/auth/user",{headers:{cookie:`${Ht}.session=sid-user; ${Ve}=${We}`}}));e(Xe?.status===200,`clear request must be 200, got ${Xe?.status}`),e((Xe?.headers.getSetCookie()??[]).some((Ye)=>Ye.startsWith(`${Ve}=`)&&Ye.includes("Max-Age=0")),"a stale profile cookie is cleared on touch after Ops disables")}},{name:"the general authenticated path does no display work for a cookie-less client",fn:async()=>{let Ne=new Vs,Le=new Date(Date.now()+3600000);await Ne.create({sid:"sid-user",sub:"user-1",authType:"sso",tenantId:"acme",createdAt:new Date,expiresAt:Le,claims:{sub:"user-1",iss:"https://idp.ecv.local",aud:"ecv",name:"Ada",email:"ada@example.com"},tokens:{access_token:"access",id_token:"id",token_type:"Bearer",expires:Le.toISOString(),scope:"openid"}});let ze=0,De=Ne.get.bind(Ne);Ne.get=async(Fe)=>(ze+=1,De(Fe));let Ve=new Xu,Je=0,Ge=Ve.lookup.bind(Ve);Ve.lookup=async(Fe)=>(Je+=1,Ge(Fe));let Ke=await Na(eo().sso,zs,void 0,{sessionStore:Ne,userStore:Ve,cookiesPrefix:Ht,cookiesSecure:!1,tokenUrl:"/auth/token",tenantId:"acme",displayCookie:{enabled:!0}})?.handler(new Request("https://app.ecv.local/auth/token",{headers:{cookie:`${Ht}.session=sid-user`}}));e(Ke?.status===200,`token endpoint must be 200, got ${Ke?.status}`),e(!(Ke?.headers.getSetCookie()??[]).some((Fe)=>Fe.startsWith(Ds(Ht))),"a cookie-less client gets no display Set-Cookie on the general path"),e(ze===1,`exactly one session read, got ${ze} (no sibling probing)`),e(Je===0,`no user-store read on the general path, got ${Je}`)}},{name:"a trim hook chooses survivors but cannot drop the cookie being written",fn:async()=>{let Ne=new Vs,Le=new Date(Date.now()+7200000),ze=new Date(Date.now()+3600000),De=new Date(Date.now()+1800000),Ve=(Fe,He,Xe)=>({profile:`ion.sso.${Fe}.profile`,session:`ion.sso.${Fe}.session`,sid:He,value:btoa(JSON.stringify({v:1,tenantId:Fe,authType:"sso",expiresAt:Xe.toISOString(),user:{}}))}),Je=Ve("newer","sid-newer",Le),Ge=Ve("older","sid-older",De);for(let[Fe,He]of[[Je,Le],[Ge,De]])await Ne.create({sid:Fe.sid,sub:"user",authType:"sso",createdAt:new Date,expiresAt:He});let We=Ds(Ht),Ke=await Ma({cookieHeader:[`${Je.profile}=${Je.value}`,`${Je.session}=${Je.sid}`,`${Ge.profile}=${Ge.value}`,`${Ge.session}=${Ge.sid}`].join("; "),sessionStore:Ne,cookiesPrefix:Ht,authType:"sso",tenantId:"acme",expiresAt:ze,user:{name:"Ada"},displayCookie:{enabled:!0,max:2},trim:(Fe)=>Fe.filter((He)=>He.name!==We),attr:La});e(!!za(Ke,We),"the cookie being written survives a hostile trim"),e(!Ke.some((Fe)=>Fe.startsWith(`${Je.profile}=`)),"the newest sibling the hook kept is untouched"),e(Ke.some((Fe)=>Fe.startsWith(`${Ge.profile}=`)&&Fe.includes("Max-Age=0")),"max still caps the total after the written cookie is re-added")}},{name:"discovered hints use a separate cap, survive without peer sessions, and dedupe on login",fn:async()=>{let Ne={...La,domain:"app.ecv.local"},ze=fc({cookieHeader:null,tenants:[{tenantId:"alpha",tenantName:"Alpha"},{tenantId:"beta",tenantName:"Beta"},{tenantId:"gamma",tenantName:"Gamma"}],authType:"sso",user:{name:"Ada",email:"ada@example.com",sid:"forbidden"},displayCookie:{enabled:!0,discovered:{enabled:!0,ttlSeconds:14400,max:2}},attr:Ne}).filter((He)=>!He.includes("Max-Age=0"));e(ze.length===2,`discovered max must be independent, got ${ze.length}`),e(ze.every((He)=>He.includes("Domain=app.ecv.local")),"set cookies carry Domain");let De=ze.map((He)=>He.slice(0,He.indexOf(";"))).join("; "),Ve=ze.map((He)=>mn(He.slice(He.indexOf("=")+1,He.indexOf(";"))));e(Ve.every((He)=>He?.kind==="discovered"),"hints carry discovered kind"),e(Ve.every((He)=>He?.user.sid===void 0),"hints strip auth material"),e(Ve[0]?.tenantName==="Alpha","hints carry the tenant label");let Je=Ds("ion.sso.alpha"),Ge="",We=0,Ke=await Ma({cookieHeader:De,sessionStore:new Vs,cookiesPrefix:"ion.sso.alpha",authType:"sso",tenantId:"alpha",expiresAt:new Date(Date.now()+60000),user:{name:"Ada"},displayCookie:{enabled:!0,max:1,discovered:{enabled:!0,max:2}},trim:(He,Xe)=>(Ge=He.map((Ye)=>Ye.kind).join(","),We=Xe.maxDiscovered,He),attr:Ne});e(Ge.includes("session,discovered"),"trim hook sees session and discovered kinds in order"),e(We===2,"trim hook receives the independent discovered cap"),e(Ke.some((He)=>He.startsWith(`${Je}=`)&&!He.includes("Max-Age=0")),"login writes the authoritative session display cookie"),e(Ke.some((He)=>He.startsWith(`${gc("alpha")}=`)&&He.includes("Max-Age=0")),"login evicts the duplicate discovered hint"),e(!Ke.some((He)=>He.startsWith(`${gc("beta")}=`)&&He.includes("Max-Age=0")),"another live discovered hint survives without a peer session");let Fe=fc({cookieHeader:De,tenants:[],authType:"sso",user:{},displayCookie:{enabled:!0,discovered:{enabled:!1}},attr:Ne});e(Fe.length===2,"disabling discovered hints clears existing discovered cookies"),e(Fe.every((He)=>He.includes("Max-Age=0")&&He.includes("Domain=app.ecv.local")),"clear cookies carry the matching Domain")}},{name:"compact selector JWTs verify across planned key overlap and fail after retirement",fn:async()=>{let Ne=ur({active:"old",keys:{old:"old-high-entropy-selector-secret",next:"next-high-entropy-selector-secret"}}),Le=await Yu({tenantId:"alpha"},Ne,{ttlSeconds:60}),ze=ur({active:"next",keys:{old:"old-high-entropy-selector-secret",next:"next-high-entropy-selector-secret"}}),De=await ka(Le,ze);e(De?.tenantId==="alpha","an overlap verifier accepts a token issued by the previous key");let Ve=ur({active:"next",keys:{next:"next-high-entropy-selector-secret"}});e(await ka(Le,Ve)===null,"retiring the old kid invalidates its tokens"),e(await ka(`${Le.slice(0,-1)}x`,ze)===null,"a tampered selector token fails closed");let Je=Math.floor(Date.now()/1000),Ge=await to({tenantId:"alpha",iat:Je+30,exp:Je+90},"next");e((await ka(Ge,ze))?.tenantId==="alpha","iat within skew tolerance verifies");let We=await to({tenantId:"alpha",iat:Je-120,exp:Je-30},"next");e((await ka(We,ze))?.tenantId==="alpha","exp within skew tolerance verifies");let Ke=await to({tenantId:"alpha",iat:Je+120,exp:Je+180},"next");e(await ka(Ke,ze)===null,"iat beyond skew tolerance fails closed");let Fe=await to({tenantId:"alpha",iat:Je-300,exp:Je-120},"next");e(await ka(Fe,ze)===null,"exp beyond skew tolerance fails closed")}},{name:"CIAM /auth/customer applies the endpoint policy and syncs the customer display cookie",fn:async()=>{let Ne=new Vs,Le=new Date(Date.now()+3600000);await Ne.create({sid:"sid-cust",sub:"cust-1",authType:"ciam",tenantId:"acme",createdAt:new Date,expiresAt:Le,claims:{sub:"cust-1",iss:"ionite.ciam",aud:"ecv",exp:Math.floor(Le.getTime()/1000),iat:Math.floor(Date.now()/1000),sid:"sid-cust",name:"Cus Tomer",email:"cus@example.com",preferred_username:"cus"},accessClaims:{sub:"cust-1",sid:"sid-cust"}});let ze=eo(),De={sessionStore:Ne,cookiesPrefix:hn,cookiesSecure:!1,tenantId:"acme"},Ve=Ds(hn),Je=()=>new Request("https://app.ecv.local/auth/customer",{headers:{cookie:`${hn}.session=sid-cust`}}),We=await Qn(ze.ciam,zs,void 0,{...De,userEndpoint:"off"})?.customerHandler(Je());e(We?.status===404,`off must be 404, got ${We?.status}`);let Fe=await Qn(ze.ciam,zs,void 0,{...De,userEndpoint:"display",displayUser:()=>({name:"Projected Customer"})})?.customerHandler(Je());e(Fe?.status===200,`display must be 200, got ${Fe?.status}`);let He=await Fe?.json();e(He.name==="Projected Customer","display policy returns the customer projection"),e(He.email===void 0,"display policy does not return the full customer");let Xe=Qn(ze.ciam,zs,void 0,{...De,userEndpoint:"full",displayCookie:{enabled:!0}}),Ye=await Xe?.customerHandler(Je());e(Ye?.status===200,`full must be 200, got ${Ye?.status}`);let Ze=await Ye?.json();e(Ze.email==="cus@example.com","full policy returns the verified customer");let tt=za(Ye?.headers.getSetCookie()??[],Ve);e(!!tt,"an enabled CIAM display cookie is written by the customer endpoint");let Qe=mn(tt);e(Qe?.authType==="ciam","the CIAM envelope carries authType ciam"),e(Qe?.tenantId==="acme","the CIAM envelope carries the tenant id"),e(Qe?.user.email==="cus@example.com","the CIAM envelope carries the default projection"),e(Qe?.user.sid===void 0,"sid must not be in the CIAM display cookie");let st=await Xe?.customerHandler(new Request("https://app.ecv.local/auth/customer",{headers:{cookie:`${hn}.session=sid-cust; ${Ve}=${tt}`}}));e(st?.status===200,`polled must be 200, got ${st?.status}`),e(!(st?.headers.getSetCookie()??[]).some((dt)=>dt.startsWith(Ve)),"a present CIAM profile cookie is not rewritten per poll");let nt=await Qn(ze.ciam,zs,void 0,{...De,userEndpoint:"full"})?.customerHandler(new Request("https://app.ecv.local/auth/customer",{headers:{cookie:`${hn}.session=sid-cust; ${Ve}=${tt}`}}));e(nt?.status===200,`disabled must still be 200, got ${nt?.status}`),e((nt?.headers.getSetCookie()??[]).some((dt)=>dt.startsWith(`${Ve}=`)&&dt.includes("Max-Age=0")),"a stale CIAM profile cookie is cleared on touch after Ops disables")}}]}}async function Ce(){let Be=performance.now(),{tests:Ne}=x(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Display cookies",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import*as ua from"node:crypto";import{CONTENT_TYPE_JSON as yc,defaultLogger as Zu}from"@ionite/core";import{clearJwksCache as wc,sso as Qu}from"@ionite/core/server";import{InMemorySessionStore as ep}from"@ionite/server";import{createValidators as tp}from"@ionite/valibot";var vc="https://idp.ecv.local/realms/ecv",bc="ecv-oidc-client",kc="ion.sso",hr=86400,pr=7*hr,sp=5000;function ap(Be){let{publicKey:Ne,privateKey:Le}=ua.generateKeyPairSync("rsa",{modulusLength:2048}),ze=Ne.export({format:"jwk"});return{privateKey:Le,kid:Be,jwk:{kty:ze.kty,n:ze.n,e:ze.e,kid:Be,use:"sig",alg:"RS256"}}}function mr(Be){return Buffer.from(Be).toString("base64url")}function np(Be,Ne){let Le={alg:"RS256",typ:"JWT",kid:Be.kid},ze=`${mr(JSON.stringify(Le))}.${mr(JSON.stringify(Ne))}`,De=ua.createSign("RSA-SHA256");return De.update(ze),`${ze}.${mr(De.sign(Be.privateKey))}`}function Sc(Be={}){let Ne=Math.floor(Date.now()/1000);return{iss:vc,aud:bc,sub:"ecv-user",iat:Ne,exp:Ne+3600,...Be}}function no(Be,Ne){if(!Be)throw Error(Ne)}function op(Be){return new Request("https://app.ecv.local/",{headers:{cookie:`${kc}.session=${Be}`}})}function rp(Be){return{sid:Be,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:Sc({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}async function so(Be){let Ne=ap(`kid-${ua.randomUUID()}`),Le=`https://jwks.ecv.local/${ua.randomUUID()}`,ze=`https://idp.ecv.local/token/${ua.randomUUID()}`,De=new ep,Ve=[],Je={async create(We){Ve.push(We.sid),await De.create(We)},get:(We)=>De.get(We),update:(We,Ke)=>De.update(We,Ke),delete:(We)=>De.delete(We)};await Je.create(rp("sid-window"));let Ge=globalThis.fetch;wc(Le),globalThis.fetch=async(We,Ke)=>{let Fe=typeof We==="string"?We:We.toString();if(Fe===Le)return new Response(JSON.stringify({keys:[Ne.jwk]}),{status:200,headers:yc});if(Fe===ze){let He=np(Ne,Sc({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}));return new Response(JSON.stringify({access_token:"new-access",id_token:He,token_type:"Bearer",refresh_token:"new-refresh",expires_in:300,...Be.refreshExpiresIn!==void 0?{refresh_expires_in:Be.refreshExpiresIn}:{},scope:"openid"}),{status:200,headers:yc})}return Ge(We,Ke)};try{let We=tp(),Ke=Qu(We.sso,Zu,void 0,{authority:vc,tokenUrl:ze,authorizationUrl:"https://idp.ecv.local/authorize",clientId:bc,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Le,sessionStore:Je,cookiesPrefix:kc,cookiesSecure:!1,sessionWindowSeconds:Be.sessionWindowSeconds},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!Ke)throw Error("sso() returned undefined");let Fe=Date.now(),He=await Ke.resolveUser(op("sid-window"));no(He.status==="authenticated",`refresh should succeed, got ${He.status}`);let Xe=Ve.find((Ze)=>Ze!=="sid-window");no(!!Xe,"refresh must rotate to a new sid");let Ye=await Je.get(Xe);return no(Ye!=null,"rotated session record must exist"),new Date(Ye.expiresAt).getTime()-Fe}finally{globalThis.fetch=Ge,wc(Le)}}function ao(Be,Ne,Le){let ze=Ne*1000;no(Math.abs(Be-ze)<=sp,`${Le}: expected a ~${Ne}s session window, got ${Math.round(Be/1000)}s`)}function E(){return{tests:[{name:"IdP refresh_expires_in always wins over the configured fallback",fn:async()=>{let Ne=await so({sessionWindowSeconds:pr,refreshExpiresIn:3600});ao(Ne,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 Ne=await so({sessionWindowSeconds:pr});ao(Ne,pr,"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 Ne=await so({});ao(Ne,hr,"default fallback window")}},{name:"non-positive sessionWindowSeconds falls back to the 86400s default",fn:async()=>{let Ne=await so({sessionWindowSeconds:0});ao(Ne,hr,"a nonsense (<1) value must not produce an instant-death window")}}]}}async function Re(){let Be=performance.now(),{tests:Ne}=E(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"SSO session window",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import*as js from"node:crypto";import{CONTENT_TYPE_JSON as Tc,defaultLogger as ip}from"@ionite/core";import{clearJwksCache as Cc,sso as cp}from"@ionite/core/server";import{InMemorySessionStore as Ec}from"@ionite/server";import{createValidators as dp}from"@ionite/valibot";var Ac="https://idp.ecv.local/realms/ecv",ro="ecv-oidc-client",oo="ecv-confidential-secret",Ic="ion.sso";function lp(Be){let{publicKey:Ne,privateKey:Le}=js.generateKeyPairSync("rsa",{modulusLength:2048}),ze=Ne.export({format:"jwk"});return{privateKey:Le,kid:Be,jwk:{kty:ze.kty,n:ze.n,e:ze.e,kid:Be,use:"sig",alg:"RS256"}}}function gr(Be){return Buffer.from(Be).toString("base64url")}function up(Be,Ne){let Le={alg:"RS256",typ:"JWT",kid:Be.kid},ze=`${gr(JSON.stringify(Le))}.${gr(JSON.stringify(Ne))}`,De=js.createSign("RSA-SHA256");return De.update(ze),`${ze}.${gr(De.sign(Be.privateKey))}`}function fr(Be={}){let Ne=Math.floor(Date.now()/1000);return{iss:Ac,aud:ro,sub:"ecv-user",iat:Ne,exp:Ne+3600,...Be}}function fs(Be,Ne){if(!Be)throw Error(Ne)}function Pc(Be){return new Request("https://app.ecv.local/",{headers:{cookie:`${Ic}.session=${Be}`}})}function pp(Be){return{sid:Be,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:fr({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}function mp(Be){return{sid:Be,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:fr({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}function _c(Be,Ne){let Le=dp(),ze=cp(Le.sso,ip,void 0,{authority:Ac,tokenUrl:Ne.tokenUrl,authorizationUrl:"https://idp.ecv.local/authorize",clientId:ro,clientSecret:Ne.clientSecret,revocationEndpoint:Ne.revocationEndpoint,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Ne.jwksUri,sessionStore:Be,cookiesPrefix:Ic,cookiesSecure:!1},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!ze)throw Error("sso() returned undefined");return ze}async function Rc(Be){let Ne=lp(`kid-${js.randomUUID()}`),Le=`https://jwks.ecv.local/${js.randomUUID()}`,ze=`https://idp.ecv.local/token/${js.randomUUID()}`,De=new Ec;await De.create(pp("sid-refresh"));let Ve=globalThis.fetch;Cc(Le);let Je;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge.toString();if(Ke===Le)return new Response(JSON.stringify({keys:[Ne.jwk]}),{status:200,headers:Tc});if(Ke===ze){Je=String(We?.body??"");let Fe=up(Ne,fr({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}));return new Response(JSON.stringify({access_token:"new-access",id_token:Fe,token_type:"Bearer",refresh_token:"new-refresh",expires_in:3600,refresh_expires_in:86400,scope:"openid"}),{status:200,headers:Tc})}return Ve(Ge,We)};try{let We=await _c(De,{tokenUrl:ze,jwksUri:Le,clientSecret:Be}).resolveUser(Pc("sid-refresh"));return fs(We.status==="authenticated",`refresh should succeed, got ${We.status}`),fs(Je!==void 0,"refresh must POST to the token endpoint"),new URLSearchParams(Je)}finally{globalThis.fetch=Ve,Cc(Le)}}async function xc(Be){let Ne=`https://jwks.ecv.local/${js.randomUUID()}`,Le=`https://idp.ecv.local/token/${js.randomUUID()}`,ze=`https://idp.ecv.local/revoke/${js.randomUUID()}`,De=new Ec;await De.create(mp("sid-logout"));let Ve=globalThis.fetch,Je;globalThis.fetch=async(Ge,We)=>{if((typeof Ge==="string"?Ge:Ge.toString())===ze)return Je=String(We?.body??""),new Response(null,{status:200});return Ve(Ge,We)};try{return await _c(De,{tokenUrl:Le,jwksUri:Ne,clientSecret:Be,revocationEndpoint:ze}).logout(Pc("sid-logout")),fs(Je!==void 0,"logout must POST the refresh token to the revocation endpoint"),new URLSearchParams(Je)}finally{globalThis.fetch=Ve}}function A(){return{tests:[{name:"confidential client: refresh_token grant authenticates with client_secret (RFC 6749 §6)",fn:async()=>{let Ne=await Rc(oo);fs(Ne.get("grant_type")==="refresh_token","grant_type must be refresh_token"),fs(Ne.get("refresh_token")==="old-refresh","refresh must send the stored refresh token"),fs(Ne.get("client_id")===ro,"refresh must send client_id"),fs(Ne.get("client_secret")===oo,"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 Ne=await Rc(void 0);fs(Ne.get("grant_type")==="refresh_token","grant_type must be refresh_token"),fs(Ne.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 Ne=await xc(oo);fs(Ne.get("token")==="live-refresh","revocation must send the stored refresh token"),fs(Ne.get("token_type_hint")==="refresh_token","revocation must hint refresh_token"),fs(Ne.get("client_id")===ro,"revocation must send client_id"),fs(Ne.get("client_secret")===oo,"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 Ne=await xc(void 0);fs(Ne.get("token")==="live-refresh","revocation must send the stored refresh token"),fs(Ne.get("client_secret")===null,"a public (PKCE) client must not send a client_secret")}}]}}async function xe(){let Be=performance.now(),{tests:Ne}=A(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"SSO token client auth",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{ACCEPT_JSON as Cs,CONTENT_TYPE_JSON as Rs,readJsonResponse as Va}from"@ionite/core";import{provisionTenant as hp}from"@ionite/devtools";function d(Be){return hp({control:{configLocator:Be.ecv.configLocator,webhookUrl:`${Be.ecv.baseUrl}/webhook`,getTenantManagementToken:()=>o(Be.ecv.baseUrl,l)},baseUrl:Be.baseUrl,tenantPath:Be.tenantPath,profile:Be.profile,tenant:Be.tenant,basePath:Be.basePath,readiness:Be.readiness})}var gp={tenantPath:"/api/tenants"};function jc(Be){let Ne=Be.headers;return{...gp,...Be,headers:async()=>({...Cs,...typeof Ne==="function"?await Ne():Ne??{}})}}function yr(Be,Ne){return`${(Be.tenantPath??"/api/tenants").replace(/\/+$/,"")}/${Ne}`}async function Uc(Be,Ne,Le,ze){return s("Tenant Create",async()=>{if(!Le)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let De={id:Le.id,ioniteId:Le.ioniteId??Le.id,companyId:Le.companyId,companyName:Le.companyName,environmentType:Le.environmentType,...Le.configLocator?{configLocator:Le.configLocator}:{},...Le.email?{email:Le.email}:{},...Le.webhookUrl||ze?{webhookUrl:Le.webhookUrl??ze}:{},...Le.callbackUrl?{callbackUrl:Le.callbackUrl}:{}},Ve=await Ne(Be.tenantPath??"/api/tenants",{method:"POST",headers:Rs,body:JSON.stringify(De)});e(Ve.status===200||Ve.status===201||Ve.status===202,`Expected 200, 201, or 202, got ${Ve.status}`);let Je=await Va(Ve);e(typeof Je.status==="string","Expected response status");let Ge=Je.status;if(e(["action_required","pending","processing","completed","failed"].includes(Ge),`Unexpected tenant status: ${Ge}`),Ge==="action_required")e(typeof Je.actionUrl==="string","Expected actionUrl for action_required"),e(typeof Je.requestToken==="string","Expected requestToken for action_required"),e(typeof Je.expiresAt==="string","Expected expiresAt for action_required");else if(Je.tenantUrl!=null)e(typeof Je.tenantUrl==="string","Expected tenantUrl to be a string when provided");return{details:{httpStatus:Ve.status}}})}async function Oc(Be,Ne,Le){return s("Tenant Get",async()=>{if(!Le)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let ze=await Ne(yr(Be,Le.id),{method:"GET",headers:Rs});e(ze.status===200,`Expected 200, got ${ze.status}`);let De=await Va(ze);return e(De.id===Le.id,"Expected id to match"),{details:{httpStatus:ze.status}}})}async function qc(Be,Ne,Le){return s("Tenant Delete",async()=>{if(!Le)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let ze=await Ne(yr(Be,Le.id),{method:"DELETE",headers:Rs});e(ze.status===204,`Expected 204, got ${ze.status}`);let De=await Ne(yr(Be,Le.id),{method:"GET"});return e(De.status===404,`Expected 404 after delete, got ${De.status}`),{details:{httpStatus:ze.status}}})}async function $c(Be,Ne){return s("Tenant Creation (Validation)",async()=>{let Le={companyId:"test-company"},ze=Be.tenantPath??"/api/tenants",De=await Ne(ze,{method:"POST",headers:Rs,body:JSON.stringify(Le)});return e(De.status===400,`Expected 400 Bad Request for invalid payload, got ${De.status}`),{details:{status:De.status}}})}async function Mc(Be,Ne){return s("Tenant Creation Rejects Malformed JSON",async()=>{let Le=Be.tenantPath??"/api/tenants",ze=await Ne(Le,{method:"POST",headers:Rs,body:'{"companyId":'});return e(ze.status===400,`Expected 400 Bad Request for malformed JSON, got ${ze.status}`),{details:{status:ze.status}}})}async function Nc(Be,Ne,Le){return s("Tenant Create Without Config Locator",async()=>{if(!Le)return{details:{skipped:!0,reason:"Test tenant data not provided"}};let ze=(Be.tenantPath??"/api/tenants").replace(/\/+$/,""),De=`${Le.id}-unconfigured`;try{let Ve=await Ne(ze,{method:"POST",headers:Rs,body:JSON.stringify({id:De,ioniteId:`${Le.ioniteId??Le.id}-unconfigured`,companyId:Le.companyId,companyName:Le.companyName,environmentType:Le.environmentType})});e(Ve.status===200||Ve.status===201||Ve.status===202,`Expected create without configLocator to succeed, got ${Ve.status}`);let Je=await Ne(`${ze}/${encodeURIComponent(De)}`,{method:"GET"});e(Je.status===200,`Expected tenant GET 200, got ${Je.status}`);let Ge=await Va(Je);return e(Ge.configLocator==null,"Expected configLocator to remain absent after create"),{details:{httpStatus:Ve.status}}}finally{await Ne(`${ze}/${encodeURIComponent(De)}`,{method:"DELETE"}).catch(()=>{return})}})}async function Lc(Be,Ne,Le){return s("Tenant Config Locator PUT Patch",async()=>{if(!Le?.configLocator)return{details:{skipped:!0,reason:"Test tenant config locator not provided"}};let ze=(Be.tenantPath??"/api/tenants").replace(/\/+$/,""),De=`${Le.id}-locator-patch`;try{let Ve=await Ne(ze,{method:"POST",headers:Rs,body:JSON.stringify({id:De,ioniteId:`${Le.ioniteId??Le.id}-locator-patch`,companyId:Le.companyId,companyName:Le.companyName,environmentType:Le.environmentType})});e(Ve.status===200||Ve.status===201||Ve.status===202,`Expected initial tenant create to succeed, got ${Ve.status}`);let Je=await Ne(`${ze}/${encodeURIComponent(De)}`,{method:"PUT",headers:Rs,body:JSON.stringify({configLocator:Le.configLocator})});e(Je.status===200||Je.status===201||Je.status===202,`Expected configLocator PUT to return 200, 201, or 202, got ${Je.status}`);let Ge=await Ne(`${ze}/${encodeURIComponent(De)}`,{method:"GET"});e(Ge.status===200,`Expected tenant GET 200, got ${Ge.status}`);let We=await Va(Ge);return e(JSON.stringify(We.configLocator)===JSON.stringify(Le.configLocator),"Expected PUT to persist the complete configLocator"),e(We.companyName===Le.companyName,"Expected PUT patch to preserve existing tenant fields"),{details:{httpStatus:Je.status}}}finally{await Ne(`${ze}/${encodeURIComponent(De)}`,{method:"DELETE"}).catch(()=>{return})}})}async function zc(Be,Ne){return s("Tenant Rejects Legacy user-tenants Route",async()=>{let Le=(Be.tenantPath??"/api/tenants").replace(/\/+$/,""),ze=await Ne(`${Le}/user-tenants?email=test@example.com`,{method:"GET",headers:Rs});return e(ze.status===404,`Expected 404, got ${ze.status}`),{details:{status:ze.status}}})}async function Ee(Be){let Ne=performance.now(),Le=jc(Be),ze=a(Le),De=Le.profile??"tenantManager",Ve=[];if(Ve.push(await Dc(Le)),Ve.push(await Vc(Le)),Ve.push(await $c(Le,ze)),Ve.push(await Mc(Le,ze)),De==="tenantManager")Ve.push(await zc(Le,ze)),Ve.push(await Nc(Le,ze,Le.testTenantData)),Ve.push(await Lc(Le,ze,Le.testTenantData));if(Ve.push(await Uc(Le,ze,Le.testTenantData,Le.webhookPath?`${n(Le)}${Le.webhookPath}`:void 0)),De==="tenantManager")Ve.push(await Oc(Le,ze,Le.testTenantData)),Ve.push(await qc(Le,ze,Le.testTenantData));let Je=performance.now()-Ne,Ge=Ve.filter((Fe)=>Fe.passed).length,We=Ve.filter((Fe)=>!Fe.passed).length,Ke=Ve.filter((Fe)=>Fe.details?.skipped).length;return{suite:"Tenant",passed:We===0,tests:Ve,duration:Je,summary:{total:Ve.length,passed:Ge-Ke,failed:We,skipped:Ke}}}async function Dc(Be){return s("Tenant Rejects Without Workload Auth",async()=>{let Ne=Be.tenantPath??"/api/tenants",ze=await a({baseUrl:Be.baseUrl,timeout:Be.timeout,headers:Cs})(Ne,{method:"POST",headers:Rs,body:JSON.stringify({id:"auth-test-app",ioniteId:"auth-test-ionite",companyId:"auth-test-company",companyName:"Auth Test Company",environmentType:"POC",configLocator:{type:"vault",vaultPath:"/ecv/test-config"}})});return e(ze.status===401,`Expected 401 without workload auth, got ${ze.status}`),{details:{httpStatus:ze.status}}})}async function Vc(Be){return s("Tenant Rejects Invalid Workload Token",async()=>{let Ne=Be.tenantPath??"/api/tenants",ze=await a({baseUrl:Be.baseUrl,timeout:Be.timeout,headers:{...Cs,Authorization:"Bearer invalid-token"}})(Ne,{method:"POST",headers:Rs,body:JSON.stringify({id:"auth-test-app",ioniteId:"auth-test-ionite",companyId:"auth-test-company",companyName:"Auth Test Company",environmentType:"POC",configLocator:{type:"vault",vaultPath:"/ecv/test-config"}})});return e(ze.status===401,`Expected 401 with invalid token, got ${ze.status}`),{details:{httpStatus:ze.status}}})}function te(Be){let Ne=jc(Be),Le=a(Ne),ze=Ne.profile??"tenantManager",De=[{name:"tenant creation endpoint rejects requests without workload authentication",fn:async()=>{let Ve=await Dc(Ne);if(!Ve.passed)throw Error(Ve.error)}},{name:"tenant creation endpoint rejects requests with invalid workload token",fn:async()=>{let Ve=await Vc(Ne);if(!Ve.passed)throw Error(Ve.error)}},{name:"tenant creation endpoint validates required fields",fn:async()=>{let Ve=await $c(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"tenant creation endpoint rejects malformed JSON",fn:async()=>{let Ve=await Mc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"tenant manager rejects legacy user-tenants route",fn:async()=>{let Ve=await zc(Ne,Le);if(!Ve.passed)throw Error(Ve.error)}},{name:"tenant create accepts an omitted config locator",fn:async()=>{let Ve=await Nc(Ne,Le,Ne.testTenantData);if(!Ve.passed&&!Ve.details?.skipped)throw Error(Ve.error)}},{name:"tenant PUT attaches a config locator without replacing other fields",fn:async()=>{let Ve=await Lc(Ne,Le,Ne.testTenantData);if(!Ve.passed&&!Ve.details?.skipped)throw Error(Ve.error)}},{name:"tenant upsert returns valid response structure",fn:async()=>{let Ve=await Uc(Ne,Le,Ne.testTenantData,Ne.webhookPath?`${n(Ne)}${Ne.webhookPath}`:void 0);if(!Ve.passed&&!Ve.details?.skipped)throw Error(Ve.error)}},{name:"tenant get returns upserted tenant",fn:async()=>{let Ve=await Oc(Ne,Le,Ne.testTenantData);if(!Ve.passed&&!Ve.details?.skipped)throw Error(Ve.error)}},{name:"tenant delete removes tenant",fn:async()=>{let Ve=await qc(Ne,Le,Ne.testTenantData);if(!Ve.passed&&!Ve.details?.skipped)throw Error(Ve.error)}}];if(ze==="creation")return De.filter((Ve)=>["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(Ve.name));return De}function Da(Be){return typeof Be.ecv==="function"?Be.ecv():Be.ecv}function gn(Be){return`${Be}-${Date.now()}-${Math.random().toString(36).slice(2,9)}`}function fn(Be,Ne){return{id:Ne,ioniteId:`ionite-${Ne}`,companyId:Ne,companyName:`ECV ${Ne}`,environmentType:"DEV",get configLocator(){return{type:"vault",vaultPath:`${Da(Be).configLocator.vaultPath}/${Ne}`}},get webhookUrl(){return`${Da(Be).baseUrl}/webhook`}}}async function co(Be){return o(Da(Be).baseUrl,l)}async function io(Be,Ne,Le){let ze=(Be.tenantPath??"/api/tenants").replace(/\/+$/,"");return fetch(new URL(`${ze}/${encodeURIComponent(Ne)}`,n(Be)),{method:"DELETE",headers:{...Cs,Authorization:`Bearer ${Le}`},signal:AbortSignal.timeout(Be.timeout??15000)})}function fp(Be){let Ne=fn(Be,gn("ecv-lifecycle"));return{name:"tenant lifecycle exposes derived URLs and tears down tenant routing",fn:async()=>{let Le=Da(Be),ze=await co(Be);try{let De=await d({ecv:Le,baseUrl:n(Be),tenantPath:Be.tenantPath,tenant:Ne,readiness:{requestTimeoutMs:Be.timeout??15000}}),Ve=(Be.tenantPath??"/api/tenants").replace(/\/+$/,""),Je=await fetch(new URL(`${Ve}/${encodeURIComponent(Ne.id)}`,n(Be)),{headers:{...Cs,Authorization:`Bearer ${ze}`},signal:AbortSignal.timeout(Be.timeout??15000)});e(Je.status===200,`Expected tenant GET 200, got ${Je.status}`);let Ge=await Va(Je);e(typeof Ge.callbackUrl==="string","Tenant GET must include callbackUrl"),e(typeof Ge.ionUrls?.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl"),e(typeof Ge.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl");let We=await io(Be,Ne.id,ze);e(We.status===204,`Expected tenant DELETE 204, got ${We.status}`);let Ke=await fetch(new URL(`${Ve}/${encodeURIComponent(Ne.id)}`,n(Be)),{headers:{...Cs,Authorization:`Bearer ${ze}`},signal:AbortSignal.timeout(Be.timeout??15000)});e(Ke.status===404,`Expected tenant GET 404 after delete, got ${Ke.status}`);let Fe=await fetch(`${De.baseUrl}${De.basePath}/readyz`,{headers:Cs,signal:AbortSignal.timeout(Be.timeout??15000)});if(e(Fe.status===503,`Expected deleted tenant route to return 503, got ${Fe.status}`),De.basePath.endsWith("/ionite")){let He=`${De.basePath.slice(0,-7)}/readyz`,Xe=await fetch(`${De.baseUrl}${He}`,{headers:Cs,signal:AbortSignal.timeout(Be.timeout??15000)});e(Xe.status===404,`Expected deleted tenant readiness to return 404, got ${Xe.status}`)}}finally{await io(Be,Ne.id,ze).catch(()=>{return})}}}}function yp(Be){let Ne=fn(Be,gn("ecv-isolation-a")),Le=fn(Be,gn("ecv-isolation-b"));return{name:"session from one tenant cannot authenticate against another tenant",fn:async()=>{let ze=Da(Be),De=await co(Be),Ve=[];try{Ve.push(Ne.id);let Je=await d({ecv:ze,baseUrl:n(Be),tenantPath:Be.tenantPath,tenant:Ne,readiness:{requestTimeoutMs:Be.timeout??15000}});Ve.push(Le.id);let Ge=await d({ecv:ze,baseUrl:n(Be),tenantPath:Be.tenantPath,tenant:Le,readiness:{requestTimeoutMs:Be.timeout??15000}}),We={baseUrl:Je.baseUrl,timeout:Be.timeout,loginPath:`${Je.basePath}/auth/login`,callbackPath:`${Je.basePath}/auth/callback`,userPath:`${Je.basePath}/auth/user`},Ke=a(We),Fe=await V(We,Ke);e(Fe.success,Fe.error??"Tenant A login failed");let He=oa(Fe.cookies);e(He.length>0,"Tenant A login did not produce a session cookie");let Xe=await Ke(We.userPath,{headers:{...Cs,Cookie:He}});e(Xe.status===200,`Tenant A session did not authenticate on tenant A: ${Xe.status}`);let Ze=await a({baseUrl:Ge.baseUrl,timeout:Be.timeout})(`${Ge.basePath}/auth/user`,{headers:{...Cs,Cookie:He}});e(Ze.status===401,`Tenant A session authenticated on tenant B (expected 401, got ${Ze.status})`)}finally{await Promise.all(Ve.map((Je)=>io(Be,Je,De).catch(()=>{return})))}}}}function wp(Be){let Ne=fn(Be,gn("ecv-integration"));return{name:"signed staged request is workload-bound and persists consent_requested without browser authority",fn:async()=>{let Le=Da(Be),ze=await co(Be),De=(Be.tenantPath??"/api/tenants").replace(/\/+$/,"");try{await d({ecv:Le,baseUrl:n(Be),tenantPath:De,tenant:Ne,readiness:{requestTimeoutMs:Be.timeout??15000}});let Ve=await Le.mintTmHandle({audience:`${Le.baseUrl}/workload`,ioniteId:"ecv-console",productId:"ecv-product",applicationId:"ecv-application",tenantHint:Ne.id}),Je=new URL(`${De}/integrations/redeem`,n(Be)),Ge=await fetch(Je,{method:"POST",headers:{...Cs,...Rs},body:JSON.stringify({handle:Ve})});e(Ge.status===401,`Expected unauthenticated redeem 401, got ${Ge.status}`);let We=new URL(`${De}/integrations/authorize`,n(Be));We.searchParams.set("handle",Ve);let Ke=await fetch(We,{redirect:"manual"});e(Ke.status!==400&&Ke.status!==401,`A valid signed authorize handle was rejected (${Ke.status})`);let Fe=await fetch(new URL(`${De}/${encodeURIComponent(Ne.id)}`,n(Be)),{headers:{...Cs,Authorization:`Bearer ${ze}`}});e(Fe.status===200,`Expected tenant details 200, got ${Fe.status}`);let He=await Va(Fe);e(He.integration?.status==="consent_requested","Expected consent_requested integration state"),e(He.integration.handleHash===void 0,"Tenant wire response exposed handleHash");let Xe=await fetch(Je,{method:"POST",headers:{...Cs,Authorization:`Bearer ${ze}`,...Rs},body:JSON.stringify({handle:Ve})});e(Xe.status===202,`Expected pending redeem 202, got ${Xe.status}`)}finally{await io(Be,Ne.id,ze).catch(()=>{return})}}}}function B(Be){let Ne=Be.profile??"tenantManager",Le,ze=fn(Be,gn("ecv-contract")),De=te({baseUrl:Be.baseUrl,timeout:Be.timeout,tenantPath:Be.tenantPath,profile:Ne,webhookPath:Be.webhookPath,headers:async()=>(Le??=co(Be),{Authorization:`Bearer ${await Le}`}),testTenantData:ze});if(Ne==="creation")return De;return[...De,fp(Be),yp(Be),wp(Be)]}import{CONTENT_TYPE_JSON as vp}from"@ionite/core";import{defineCatalog as bp,getOwnedTenantIds as kp,InMemoryTenantStore as Sp,inMemoryAuthzStores as Tp,ionite as Wc,materializeTenantOwnerEntitlement as Bc,purgeTenantEntitlements as Cp,resolveTenantOwner as Rp,resolveTenantSession as Sa,TENANT_OWNER_PERMISSION as Jc,tenantOwnerEntitlementId as Gc,tenantOwnerPermission as xp,tenantResourceId as wr}from"@ionite/server";import{createValidators as br}from"@ionite/valibot";function Kc(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function Ps(Be){let Ne=Be.decision??{allow:!1};return{isReady:()=>Be.ready??!0,sso:Be.user?{resolveUser:async()=>Be.user}:void 0,ciam:Be.customer?{resolveCustomer:async()=>Be.customer}:void 0,authz:{hasPermission:async()=>Ne}}}async function Ep(Be){let Ne=Wc(Kc({id:Be}),{validators:br(),stores:{catalog:bp({permissions:[xp()]}),...Tp()}});return await Ne.ready(1000),Ne}function vr(Be){let Ne=typeof Be==="string"?new TextEncoder().encode(Be):new Uint8Array(Be);return btoa(String.fromCharCode(...Ne)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async function Ap(){let Be=await crypto.subtle.generateKey({name:"RSASSA-PKCS1-v1_5",modulusLength:2048,publicExponent:new Uint8Array([1,0,1]),hash:"SHA-256"},!0,["sign","verify"]),Ne=`ecv-${crypto.randomUUID()}`,Le=await crypto.subtle.exportKey("jwk",Be.publicKey),ze=`data:application/json,${encodeURIComponent(JSON.stringify({keys:[{...Le,kid:Ne,alg:"RS256",use:"sig"}]}))}`,De="https://tenant-access.ecv.test",Ve="tenant-access-app",Je="tenant-access-ionite",Ge=Math.floor(Date.now()/1000),We=vr(JSON.stringify({alg:"RS256",kid:Ne,typ:"JWT"})),Ke=vr(JSON.stringify({iss:"https://tenant-access.ecv.test",aud:"tenant-access-app",azp:"tenant-access-ionite",iat:Ge,exp:Ge+300,sub:"tenant-access-ionite"})),Fe=await crypto.subtle.sign({name:"RSASSA-PKCS1-v1_5"},Be.privateKey,new TextEncoder().encode(`${We}.${Ke}`));return{config:{id:"tenant-access-tm",tm:{enabled:!0,ionite:{id:"ecv",url:"https://tenant-access.ecv.test",issuer:"https://tenant-access.ecv.test",jwksUri:ze,audience:"tenant-access-app",clientId:"tenant-access-ionite"}}},token:`${We}.${Ke}.${vr(Fe)}`}}function I(){return{tests:[{name:"tenant session cascade returns 404 for an unknown tenant and allows an SSO user",fn:async()=>{let Be=Ps({}),Ne=await Sa(new Request("https://app.test"),{rootIon:Be,tenantIon:void 0,tenantId:"missing"});if(e(!Ne.allow,"missing tenant should deny"),!Ne.allow)t(Ne.response().status,404,"missing tenant status");let Le=Ps({user:{status:"authenticated",value:{id:"user-1",email:"user@example.test"}},decision:{allow:!0}}),ze=await Sa(new Request("https://app.test"),{rootIon:Be,tenantIon:Le,tenantId:"acme",permission:"app.admin"});e(ze.allow&&ze.session.kind==="sso","SSO admin should satisfy the first cascade leg")}},{name:"tenant session cascade preserves permission deny and degraded identity semantics",fn:async()=>{let Be=await Sa(new Request("https://app.test"),{rootIon:Ps({}),tenantIon:Ps({user:{status:"authenticated",value:{id:"user-1"}},decision:{allow:!1}}),tenantId:"acme",permission:"app.admin"});if(e(!Be.allow,"permission deny should reject"),!Be.allow)t(Be.response().status,403,"permission deny status");let Ne=await Sa(new Request("https://app.test"),{rootIon:Ps({}),tenantIon:Ps({user:{status:"degraded",error:Error("session store down")}}),tenantId:"acme"});if(e(!Ne.allow,"degraded identity should reject"),!Ne.allow)t(Ne.response().status,503,"degraded identity status")}},{name:"owner fallback runs before readiness failure and owner-only rejects an ungranted customer",fn:async()=>{let Be=new Request("https://app.test"),Ne=Ps({customer:{status:"authenticated",value:{id:"customer-1"}},decision:{allow:!0}}),Le=await Sa(Be,{rootIon:Ne,tenantIon:Ps({ready:!1}),tenantId:"acme"});e(Le.allow&&Le.session.kind==="owner","owner fallback should work while tenant ion is unready");let ze=await Rp(Be,{rootIon:Ps({customer:{status:"authenticated",value:{id:"customer-1"}},decision:{allow:!1}}),tenantId:"acme"});if(e(!ze.allow,"ungranted customer should not satisfy owner-only access"),!ze.allow)t(ze.response().status,403,"owner-only deny status")}},{name:"tenant session returns 503 only after owner fallback fails, otherwise 401 when ready",fn:async()=>{let Be=new Request("https://app.test"),Ne=Ps({customer:{status:"unauthenticated"}}),Le=await Sa(Be,{rootIon:Ne,tenantIon:Ps({ready:!1}),tenantId:"acme"});if(e(!Le.allow,"unready tenant without owner should deny"),!Le.allow)t(Le.response().status,503,"unready tenant status");let ze=await Sa(Be,{rootIon:Ne,tenantIon:Ps({ready:!0,user:{status:"unauthenticated"}}),tenantId:"acme"});if(e(!ze.allow,"ready tenant without session should deny"),!ze.allow)t(ze.response().status,401,"ready tenant status")}},{name:"owner entitlement lifecycle materializes, lists, checks, and fully cleans up",fn:async()=>{let Be=await Ep("tenant-owner-lifecycle");try{let Ne=await Bc(Be,{tenantId:"acme",displayName:"Acme"}),Le=await Bc(Be,{tenantId:"acme",displayName:"Changed presentation does not overwrite"});t(Ne.id,Le.id,"materialization is idempotent"),e(Ne.requestable===!0,"owner entitlement is requestable"),await Be.authz.grant({subject:"customer-1",subjectType:"customer",targetType:"group",target:Gc("acme"),resources:[wr("acme")]}),e((await Be.authz.hasPermission("customer-1",Jc,wr("acme"),"customer")).allow,"owner permission should be materialized for the tenant resource"),t((await kp(Be,"customer-1")).join(","),"acme","owned tenant enumeration");let ze=await Cp(Be,{tenantId:"acme"});t(ze.revoked,1,"one owner grant revoked"),t(ze.deleted,1,"one tenant entitlement deleted"),e(!(await Be.authz.hasPermission("customer-1",Jc,wr("acme"),"customer")).allow,"owner access should be gone after cleanup"),e(!await Be.authz.catalogStore?.groups.get(Gc("acme")),"owner catalog row should be gone after cleanup")}finally{Be.close()}}},{name:"Tenant Management invokes tenant create and delete lifecycle hooks",fn:async()=>{let Be=await Ap(),Ne=new Sp,Le=[],ze=Wc(Kc(Be.config),{validators:br(),tm:{basePath:"/api/tenants",store:Ne,validators:br(),onTenantCreated:({tenant:De})=>{Le.push(`create:${De.id}`)},onTenantDeleted:({tenantId:De})=>{Le.push(`delete:${De}`)}}});try{await ze.ready(1000);let De={Authorization:`Bearer ${Be.token}`,...vp},Ve=await ze.tm?.handler(new Request("https://app.test/api/tenants",{method:"POST",headers:De,body:JSON.stringify({id:"acme",ioniteId:"ionite-acme",companyId:"company-acme",companyName:"Acme",environmentType:"DEV"})}));t(Ve?.status,201,"TM create status");let Je=await ze.tm?.handler(new Request("https://app.test/api/tenants/acme",{method:"DELETE",headers:De}));t(Je?.status,204,"TM delete status"),t(Le.join(","),"create:acme,delete:acme","TM lifecycle hook order")}finally{ze.close()}}}]}}async function Ae(){let Be=performance.now(),Ne=[];for(let ze of I().tests)Ne.push(await s(ze.name,async()=>{await ze.fn();return}));let Le=Ne.filter((ze)=>!ze.passed).length;return{suite:"Tenant access",passed:Le===0,tests:Ne,duration:performance.now()-Be,summary:{total:Ne.length,passed:Ne.length-Le,failed:Le,skipped:0}}}import{CONTENT_TYPE_JSON as Hc,defaultLogger as Ip,list as Pp,remoteConfigSecretType as kr}from"@ionite/core";import{generateLfvEd25519KeyPair as yn,signLfvPayload as Yc,stringifyLfvJwks as lo}from"@ionite/core/server";import{ionite as uo,lfvVault as wn,mirroredConfig as _p,mirrorSecrets as jp,tenantMirror as Up}from"@ionite/server";import{createValidators as po}from"@ionite/valibot";var ts={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 mo{constructor(){this.values=new Map}async put(Be,Ne,Le){this.values.set(Be,Ne)}async take(Be){let Ne=this.values.get(Be);return this.values.delete(Be),Ne}}async function vn(Be,Ne,Le,ze){let De="issued_at"in Ne?Ne:{...Ne,issued_at:Date.now()},Ve=JSON.stringify(De),Je=String(Ne.request_id),Ge=await Yc({privateKeyJwk:Le,requestId:Je,rawBody:Ve});return new Request(`http://localhost${Be}`,{method:"POST",headers:{...Hc,"X-LFV-Request-Id":Je,"X-LFV-Key-Id":ze,"X-LFV-Signature":Ge},body:Ve})}async function Fc(Be,Ne,Le){let ze=String(Be.request_id),De=JSON.stringify(Be),Ve=await Yc({privateKeyJwk:Ne,requestId:ze,rawBody:De});return new Response(De,{status:200,headers:{...Hc,"X-LFV-Request-Id":ze,"X-LFV-Key-Id":Le,"X-LFV-Signature":Ve}})}async function bn(Be,Ne,Le=1000){let ze=Date.now()+Le;while(Date.now()<ze){if(Be())return;await new Promise((De)=>setTimeout(De,5))}e(Be(),Ne)}function Ba(Be){let Ne=new Map;for(let[Je,Ge]of Object.entries(Be))Ne.set(Je,{data:Ge,metadata:{path:Je,version:1,created:new Date,contentType:kr.toRegistryEntry()}});let Le=new Map,ze=new Set,De=(Je)=>{let Ge=Ne.get(Je);if(!Ge)throw Error(`not found: ${Je}`);return Ge};return{source:{type:"vault",isReady:()=>!0,ready:async()=>{},isAvailable:()=>!0,get:()=>{throw Error("not implemented")},getFullSecret:async(Je)=>De(Je),getSecret:async(Je)=>De(Je).data,putSecret:async(Je,Ge)=>{let We=Ne.get(Je);Ne.set(Je,{data:Ge,metadata:{path:Je,version:(We?.metadata.version??0)+1,created:new Date,contentType:kr.toRegistryEntry()}})},deleteSecret:async(Je)=>{Ne.delete(Je)},listPaths:async(Je)=>Array.from(Ne.keys()).filter((Ge)=>Ge.startsWith(Je)),exists:async(Je)=>Ne.has(Je),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(Je)=>De(Je).metadata,subscribe:async(Je,Ge)=>{let We=Le.get(Je)??new Set,Ke=Ge;return We.add(Ke),Le.set(Je,We),Ke(De(Je)),()=>{We.delete(Ke)}},subscribeEvents:(Je)=>(ze.add(Je),()=>ze.delete(Je))},values:Ne,emit(Je,Ge,We){let Ke={data:Ge,metadata:{path:Je,version:We,created:new Date,contentType:kr.toRegistryEntry()}};Ne.set(Je,Ke);for(let Fe of Le.get(Je)??[])Fe(Ke)},emitEvent(Je){for(let Ge of ze)Ge(Je)}}}function Sr(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function Op(Be){let Ne=Be,Le;return{async load(){return Ne},async subscribe(ze){return Le=ze,()=>{Le=void 0}},push(ze){Ne=ze,Le?.(ze)},secret:{}}}function Tr(Be){return{LFV_NAMED:{type:"vault",lfv:{lfvServerUrl:ts.lfvServerUrl,clientId:ts.clientId,path:Be,deliveryEndpoint:ts.deliveryEndpoint,eventsEndpoint:ts.eventsEndpoint}}}}function P(){return{tests:[{name:"RemoteConfig LFV secrets without a callback transport reject apply unless singleProcess is declared",fn:async()=>{let Be={id:"ecv-lfv-transport-gate",secrets:Tr("/ecv/coordination/apply-reject")},Ne=uo(Sr(Be),{validators:po()}),Le=!1,ze="";try{await Ne.ready(2000)}catch(De){Le=!0,ze=De instanceof Error?De.message:String(De)}Ne.close?.(),e(Le,"LFV secrets without a callback transport must reject ion.ready()"),e(ze.includes("callbackTransport")&&ze.includes("singleProcess"),`apply rejection should name callbackTransport and the singleProcess opt-out (got: ${ze})`)}},{name:"RemoteConfig LFV secrets apply when singleProcess is declared or a callback transport is wired",fn:async()=>{let Be={id:"ecv-lfv-transport-ok",secrets:Tr("/ecv/coordination/apply-ok")},Ne=uo(Sr(Be),{validators:po(),lfv:{singleProcess:!0}});await Ne.ready(2000),e(Ne.isReady(),"singleProcess declaration must allow LFV apply without a transport"),Ne.close?.();let Le=uo(Sr(Be),{validators:po(),lfv:{callbackTransport:new mo}});await Le.ready(2000),e(Le.isReady(),"a wired callback transport must allow LFV apply"),Le.close?.()}},{name:"a live RemoteConfig that introduces LFV without a transport keeps last-known-good",fn:async()=>{let Be=Op({id:"ecv-lfv-transport-lkg",secrets:{primary:{type:"vault",url:"http://localhost/vault"}}}),Ne=uo(Be,{validators:po()});await Ne.ready(2000),e(Ne.isReady(),"initial HTTP vault secrets apply without a callback transport"),e(Ne.secrets?.listSecretsSources().includes("primary")===!0,"last-known-good keeps the HTTP vault source"),Be.push({id:"ecv-lfv-transport-lkg",secrets:Tr("/ecv/coordination/apply-lkg")}),await new Promise((ze)=>setTimeout(ze,30)),e(Ne.isReady(),"the instance stays ready after a rejected LFV live push"),e(Ne.secrets?.listSecretsSources().includes("primary")===!0,"last-known-good HTTP vault source is retained"),e(Ne.secrets?.listSecretsSources().includes("LFV_NAMED")!==!0,"the untransportable LFV source is not applied");let Le=Ne.configStatus.lastError?.message??"";e(Le.includes("callbackTransport"),`configStatus.lastError should name the missing callback transport (got: ${Le})`),Ne.close?.()}},{name:"distributed LFV callbacks resolve an OTP waiter on a different replica",fn:async()=>{let Be=new mo,{privateJwk:Ne,publicJwks:Le}=await yn("ecv-coordination-kid"),ze={...ts,verifyJwks:lo(Le),deliveryTimeout:1000},De=wn(ze,Be),Ve=wn(ze,Be),Je=globalThis.fetch;globalThis.fetch=async(Ge,We)=>{let Ke=typeof Ge==="string"?Ge:Ge instanceof URL?Ge.toString():Ge.url,Fe=JSON.parse(String(We?.body));if(Ke.endsWith("/otp")){let He=await vn(ts.deliveryEndpoint,{type:"otp",request_id:Fe.request_id,otp:`otp-${Fe.request_id}`,action:Fe.action,path:Fe.path},Ne,"ecv-coordination-kid");return t((await Ve.handleDelivery(He)).status,200,"ingress replica accepts signed OTP"),Response.json({status:"pending"},{status:202})}if(Ke.endsWith("/secrets/request"))return Fc({request_id:Fe.request_id,status:"completed",path:Fe.path,data:{data:{token:"coordinated"},metadata:{path:Fe.path,version:1}}},Ne,"ecv-coordination-kid");throw Error(`Unexpected LFV request: ${Ke}`)};try{let Ge=await De.getSecret(ts.path);t(Ge.token,"coordinated","requesting replica receives callback through transport")}finally{globalThis.fetch=Je,De.close?.(),Ve.close?.()}}},{name:"mirrored LFV self-redeems on the callback replica without a transport or waiter",fn:async()=>{let{privateJwk:Be,publicJwks:Ne}=await yn("ecv-self-redeem-kid"),Le=wn({...ts,verifyJwks:lo(Ne),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),ze=globalThis.fetch,De=0,Ve,Je=await Le.subscribePassive(ts.path,(Ge)=>{Ve=Ge});globalThis.fetch=async(Ge,We)=>{let Ke=JSON.parse(String(We?.body));return De+=1,Fc({request_id:Ke.request_id,status:"completed",path:Ke.path,data:{data:{token:"self-redeemed"},metadata:{path:Ke.path,version:1}}},Be,"ecv-self-redeem-kid")};try{let Ge=crypto.randomUUID(),We=await Le.handleDelivery(await vn(ts.deliveryEndpoint,{type:"otp",request_id:Ge,otp:`otp-${Ge}`,action:"read_secret",path:ts.path},Be,"ecv-self-redeem-kid"));t(We.status,200,"the callback replica accepts and redeems the OTP"),t(De,1,"the callback replica redeems exactly once without a mailbox"),t(Ve?.data.token,"self-redeemed","the pulled value is published to the mirror subscriber")}finally{Je(),globalThis.fetch=ze,Le.close?.()}}},{name:"already-used self-redeem callback is terminally accepted and its replay claim survives",fn:async()=>{let{privateJwk:Be,publicJwks:Ne}=await yn("ecv-used-otp-kid"),Le=wn({...ts,verifyJwks:lo(Ne),deliveryTimeout:1000},void 0,{selfRedeemReads:!0}),ze=globalThis.fetch;globalThis.fetch=async()=>Response.json({error:"otp_already_used"},{status:409});let De=crypto.randomUUID(),Ve={type:"otp",request_id:De,otp:`otp-${De}`,action:"read_secret",path:ts.path,issued_at:Date.now()};try{let Je=await Le.handleDelivery(await vn(ts.deliveryEndpoint,Ve,Be,"ecv-used-otp-kid"));t(Je.status,200,"already-used OTP is a terminal accepted callback, not a retryable 503");let Ge=await Le.handleDelivery(await vn(ts.deliveryEndpoint,Ve,Be,"ecv-used-otp-kid"));t(Ge.status,401,"the terminal callback keeps its local replay claim")}finally{globalThis.fetch=ze,Le.close?.()}}},{name:"distributed envelopes are re-verified and do not cross a JWKS trust boundary",fn:async()=>{let Be=new mo,Ne=await yn("trusted-kid"),Le=await yn("untrusted-kid"),ze=wn({...ts,verifyJwks:lo(Ne.publicJwks),deliveryTimeout:80},Be),De=globalThis.fetch,Ve="";globalThis.fetch=async(Je,Ge)=>(Ve=JSON.parse(String(Ge?.body)).request_id??"",Response.json({status:"pending"},{status:202}));try{let Je=ze.getSecret(ts.path,{timeout:80});await bn(()=>Ve.length>0,"source must issue an OTP request");let Ge=Ve,We=await vn(ts.deliveryEndpoint,{type:"otp",request_id:Ge,otp:"attacker-otp",action:"read_secret",path:ts.path},Le.privateJwk,"untrusted-kid"),Ke=await We.text();await Be.put(Ge,{kind:"delivery",key:ze.callbackKey,rawBody:Ke,headers:{requestId:Ge,keyId:"untrusted-kid",signature:We.headers.get("X-LFV-Signature")??""}},1000);let Fe=!1;try{await Je}catch{Fe=!0}e(Fe,"untrusted distributed OTP must not resolve the waiter")}finally{globalThis.fetch=De,ze.close?.()}}},{name:"OEM mirror converges values and removes revoked material",fn:async()=>{let Be=Ba({"payments/api":{token:"v1"}}),Ne=Ba({}),Le=jp({from:Be.source,to:Ne.source,paths:["payments/api"],mapPath:(ze)=>`tenants/customer-a/${ze}`});try{await Le.ready(1000);let ze="tenants/customer-a/payments/api";t((Ne.values.get(ze)?.data).token,"v1","initial value is re-stored"),e(!Ne.values.has(`${ze}/__ionite_mirror`),"mirrors store no provenance sidecar");let De=Ne.values.get(ze)?.metadata.version;Be.emit("payments/api",{token:"v1"},1),await new Promise((Ve)=>setTimeout(Ve,20)),t(Ne.values.get(ze)?.metadata.version,De,"identical replay does not mint a new destination version"),Be.emit("payments/api",{token:"v2"},2),await bn(()=>Ne.values.get(ze)?.data?.token==="v2","rotation must update mirror"),Be.emitEvent({path:"payments/api",event:"revoked"}),await bn(()=>!Ne.values.has(ze),"revocation must remove mirrored value")}finally{await Le.close()}}},{name:"mirroredConfig loads only from the durable destination",fn:async()=>{let Be=Ba({"/authoritative/root":{id:"root-v1",readyz:{enabled:!0}}}),Ne=Ba({}),Le=_p({from:Be.source,fromPath:"/authoritative/root",to:Ne.source,path:"/mirror/root"});try{await Le.mirror.ready(1000),t((await Le.load()).id,"root-v1","config loads from the mirrored destination"),Be.emit("/authoritative/root",{id:"root-v2"},2),await bn(()=>Ne.values.get("/mirror/root")?.data?.id==="root-v2","upstream changes must converge into the destination")}finally{await Le.secret.close?.()}}},{name:"tenantMirror mirrors and purges one tenant without store-owned hooks",fn:async()=>{let Be=Ba({"/tenant/acme/config":{id:"acme-config"}}),Ne=Ba({}),Le={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"}},De=Up({log:Ip},{store:{get:async(Ve)=>Ve===Le.id?Le:void 0,list:async()=>Pp(1,[Le],0,void 0),upsert:async()=>Le,delete:async()=>1,getIon:async()=>{return},getByIoniteId:async()=>{return}},to:Ne.source,path:({id:Ve})=>`/mirrors/${Ve}/config`,sourceFromLocator:()=>Be.source});try{await De.ensure("acme"),await bn(()=>Ne.values.get("/mirrors/acme/config")?.data?.id==="acme-config","tenant config must be mirrored"),e(!Ne.values.has("/mirrors/acme/config/__ionite_mirror"),"mirrors store no provenance sidecar"),await De.purge("acme"),e(!Ne.values.has("/mirrors/acme/config"),"purge removes the tenant config")}finally{await De.close()}}}]}}async function Ie(){let Be=performance.now(),Ne=[];for(let Ve of P().tests)Ne.push(await s(Ve.name,async()=>{await Ve.fn();return}));let Le=performance.now()-Be,ze=Ne.filter((Ve)=>!Ve.passed).length,De=Ne.filter((Ve)=>Ve.details?.skipped).length;return{suite:"Vault coordination",passed:ze===0,tests:Ne,duration:Le,summary:{total:Ne.length,passed:Ne.length-ze-De,failed:ze,skipped:De}}}import{closeAllVaultSockets as Xc,setVaultWebSocketCommandTimeout as Zc,vault as xs,vaultSocketDiagnostics as ho}from"@ionite/server";import{WebSocketServer as qp}from"ws";var $p={"tok-a":"CLT-A","tok-b":"CLT-B","tok-solo":"CLT-SOLO"};function Mp(Be){return typeof Be==="string"?$p[Be]:void 0}function go(Be,Ne){return`${Be}::${Ne}`}function Np(Be){let Ne=Be["x-vault-token"],Le=Array.isArray(Ne)?Ne[0]:Ne;if(Le)return Le;let ze=Be.authorization,De=Array.isArray(ze)?ze[0]:ze;if(De)return De.replace(/^Bearer\s+/i,"");return}async function Lp(){let Be=new Map,Ne=new Set,Le=new Set,ze=new Set,De=!1,Ve=new Map,Je=0,Ge=new qp({port:0,verifyClient:(Ye,Ze)=>{let tt=Np(Ye.req.headers);if(tt&&ze.has(tt)){Ze(!1,401,"revoked credential");return}Ze(!0)}});await new Promise((Ye)=>Ge.once("listening",Ye));let We=Ge.address();if(typeof We==="string"||!We)throw Error("failed to bind test vault ws server");let Ke=`ws://127.0.0.1:${We.port}`,Fe=(Ye,Ze)=>{if(Ye.readyState===Ye.OPEN)Ye.send(JSON.stringify(Ze))},He=(Ye,Ze,tt,Qe)=>{let st=Be.get(go(Ye,Ze));if(!st)return;return{type:"secret",subscription_id:tt,client_id:Ye,event:Qe,path:Ze,subscription:Ze,data:st.data,metadata:De?{path:Ze,created_time:new Date().toISOString()}:{path:Ze,version:st.version,created_time:new Date().toISOString()}}};return Ge.on("connection",(Ye)=>{Je+=1,Le.add(Ye),Ve.set(Ye,new Map),Ye.on("message",(Ze)=>{let tt;try{tt=JSON.parse(Ze.toString())}catch{return}let Qe=tt.type,st=Mp(tt.token);if(Qe==="subscribe"){let et=String(tt.subscription_id??""),nt=Array.isArray(tt.paths)?String(tt.paths[0]):"";if(!st){Fe(Ye,{type:"error",subscription_id:et,error:"invalid token"});return}Ve.get(Ye)?.set(et,{clientId:st,path:nt});let dt=He(st,nt,et,"subscribed");if(dt)Fe(Ye,dt);return}if(Qe==="unsubscribe"){Ve.get(Ye)?.delete(String(tt.subscription_id??""));return}if(Qe==="command"){let et=String(tt.id??""),nt=String(tt.path??"");if(Ne.has(nt))return;if(!st){Fe(Ye,{type:"command_result",id:et,ok:!1,error:"invalid token",code:"auth",status:403});return}if(tt.command==="read_secret"){let dt=Be.get(go(st,nt));if(!dt){Fe(Ye,{type:"command_result",id:et,ok:!1,error:"not found",code:"not_found",status:404});return}Fe(Ye,{type:"command_result",id:et,ok:!0,result:{path:nt,data:dt.data,metadata:De?{path:nt,created_time:new Date().toISOString()}:{path:nt,version:dt.version,created_time:new Date().toISOString()}}});return}Fe(Ye,{type:"command_result",id:et,ok:!0,result:{}});return}}),Ye.on("close",()=>{Le.delete(Ye),Ve.delete(Ye)}),Fe(Ye,{type:"connected"})}),{url:Ke,connectionCount:()=>Je,openCount:()=>Le.size,setSecret:(Ye,Ze,tt)=>{let Qe=go(Ye,Ze),st=(Be.get(Qe)?.version??0)+1;Be.set(Qe,{data:tt,version:st})},pushUpdate:(Ye,Ze,tt)=>{let Qe=go(Ye,Ze),st=(Be.get(Qe)?.version??0)+1;Be.set(Qe,{data:tt,version:st});for(let[et,nt]of Ve)for(let[dt,pt]of nt){if(pt.clientId!==Ye||pt.path!==Ze)continue;let ct=He(Ye,Ze,dt,"updated");if(ct)Fe(et,ct)}},dropSockets:()=>{for(let Ye of Array.from(Le))Ye.terminate()},ignore:Ne,setOmitVersions:(Ye)=>{De=Ye},revokeToken:(Ye)=>{ze.add(Ye)},close:()=>new Promise((Ye)=>{for(let Qe of Ge.clients)Qe.terminate();let Ze=!1,tt=()=>{if(Ze)return;Ze=!0,Ye()};Ge.close(()=>tt()),setTimeout(tt,250)})}}function Qc(Be){return new Promise((Ne)=>setTimeout(Ne,Be))}async function _s(Be,Ne=5000,Le=50){let ze=Date.now();while(Date.now()-ze<Ne){if(Be())return!0;await Qc(Le)}return Be()}async function Bs(Be){let Ne=await Lp();try{await Be(Ne)}finally{Xc(),Zc(30000),await Ne.close()}}function _(){return{tests:[{name:"multiple sources on one URL share a single socket and demux by subscription_id",fn:async()=>{await Bs(async(Ne)=>{Ne.setSecret("CLT-A","/shared",{who:"A"}),Ne.setSecret("CLT-B","/shared",{who:"B"});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-a",clientId:"CLT-A"}}),ze=xs({type:"vault",websocket:{url:Ne.url,token:"tok-b",clientId:"CLT-B"}}),De=[],Ve=[],Je=await Le.subscribe("/shared",(Fe)=>De.push(Fe.data)),Ge=await ze.subscribe("/shared",(Fe)=>Ve.push(Fe.data)),We=ho();t(We.sockets,1,"exactly one socket per URL across both sources"),t(We.subscriptions,2,"two registered subscriptions on the shared socket"),t(Ne.connectionCount(),1,"server accepted exactly one connection"),e(De.some((Fe)=>Fe?.who==="A"),"client A receives its own tenant-scoped secret"),e(Ve.some((Fe)=>Fe?.who==="B"),"client B receives a different secret on the same path"),Je(),Ge();let Ke=await _s(()=>ho().sockets===0&&Ne.openCount()===0);e(Ke,"socket idle-closes after the last subscription is removed")})}},{name:"per-frame auth rejects an invalid token (fail-fast subscribe throws)",fn:async()=>{await Bs(async(Ne)=>{let Le=xs({type:"vault",websocket:{url:Ne.url,token:"not-valid",clientId:"CLT-A"}}),ze=!1;try{await Le.subscribe("/shared",()=>{},{mode:"fail-fast"})}catch{ze=!0}e(ze,"subscribe with an invalid per-frame token must reject")})}},{name:"live update is delivered and deduplicated by version",fn:async()=>{await Bs(async(Ne)=>{Ne.setSecret("CLT-SOLO","/live",{v:1});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-solo",clientId:"CLT-SOLO"}}),ze=[],De=await Le.subscribe("/live",(Ve)=>ze.push(Ve.data.v));e(await _s(()=>ze.includes(1)),"initial value delivered"),Ne.pushUpdate("CLT-SOLO","/live",{v:2}),e(await _s(()=>ze.includes(2)),"updated value delivered"),De()})}},{name:"reconnect replays active subscriptions after an unexpected socket drop",fn:async()=>{await Bs(async(Ne)=>{Ne.setSecret("CLT-SOLO","/recon",{v:1});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-solo",clientId:"CLT-SOLO"}}),ze=[],De=await Le.subscribe("/recon",(Je)=>ze.push(Je.data.v));e(await _s(()=>ze.includes(1)),"initial value delivered before drop"),Ne.setSecret("CLT-SOLO","/recon",{v:2}),Ne.dropSockets();let Ve=await _s(()=>ze.includes(2),8000);e(Ve,"subscription replayed and newer secret delivered after reconnect"),De()})}},{name:"a command with no reply rejects after the per-command timeout",fn:async()=>{await Bs(async(Ne)=>{Zc(250),Ne.ignore.add("/never");let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-solo",clientId:"CLT-SOLO"}}),ze=!1;try{await Le.getFullSecret("/never")}catch{ze=!0}e(ze,"an unanswered websocket command must reject on timeout")})}},{name:"close() releases subscriptions and idle-closes the shared socket",fn:async()=>{await Bs(async(Ne)=>{Ne.setSecret("CLT-SOLO","/closing",{v:1});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-solo",clientId:"CLT-SOLO"}});await Le.subscribe("/closing",()=>{}),t(ho().sockets,1,"socket open while subscribed"),await Le.close?.();let ze=await _s(()=>ho().sockets===0&&Ne.openCount()===0);e(ze,"close() releases the subscription and the socket idle-closes")})}},{name:"version-less deliveries are never deduplicated (server omits metadata.version)",fn:async()=>{await Bs(async(Ne)=>{Ne.setOmitVersions(!0),Ne.setSecret("CLT-SOLO","/nov",{n:1});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-solo",clientId:"CLT-SOLO"}}),ze=[],De=await Le.subscribe("/nov",(Je)=>ze.push(Je.data.n));e(await _s(()=>ze.includes(1)),"initial version-less value delivered");let Ve=ze.length;Ne.pushUpdate("CLT-SOLO","/nov",{n:2}),Ne.pushUpdate("CLT-SOLO","/nov",{n:2}),e(await _s(()=>ze.length>=Ve+2),"both version-less updates must be delivered (no version → no dedup)"),e(ze.slice(Ve,Ve+2).every((Je)=>Je===2),"the delivered version-less updates carry the pushed value"),De()})}},{name:"a throwing onChange is contained; sibling subscriptions still deliver",fn:async()=>{await Bs(async(Ne)=>{Ne.setSecret("CLT-A","/boom",{v:1}),Ne.setSecret("CLT-B","/safe",{v:1});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-a",clientId:"CLT-A"}}),ze=xs({type:"vault",websocket:{url:Ne.url,token:"tok-b",clientId:"CLT-B"}}),De=[],Ve=await Le.subscribe("/boom",()=>{throw Error("intentional onChange failure")}),Je=await ze.subscribe("/safe",(Ge)=>De.push(Ge.data.v));e(await _s(()=>De.includes(1)),"sibling receives initial value despite the throwing peer"),Ne.pushUpdate("CLT-A","/boom",{v:2}),Ne.pushUpdate("CLT-B","/safe",{v:2}),e(await _s(()=>De.includes(2)),"sibling still receives updates after the throwing subscription fired"),Ve(),Je()})}},{name:"reconnect rotates to a second credential when the first is revoked",fn:async()=>{await Bs(async(Ne)=>{Ne.setSecret("CLT-A","/rot-a",{v:1}),Ne.setSecret("CLT-B","/rot-b",{v:1});let Le=xs({type:"vault",websocket:{url:Ne.url,token:"tok-a",clientId:"CLT-A"}}),ze=xs({type:"vault",websocket:{url:Ne.url,token:"tok-b",clientId:"CLT-B"}}),De=[],Ve=await Le.subscribe("/rot-a",()=>{}),Je=await ze.subscribe("/rot-b",(We)=>De.push(We.data.v));e(await _s(()=>De.includes(1)),"initial delivery before the credential is revoked"),Ne.revokeToken("tok-a"),Ne.setSecret("CLT-B","/rot-b",{v:2}),Ne.dropSockets();let Ge=await _s(()=>De.includes(2),20000);e(Ge,"reconnect recovered via the second (still-valid) credential and replayed subscriptions"),Ve(),Je()})}},{name:"wait-mode subscribe rejects when the socket is disposed before the first secret",fn:async()=>{await Bs(async(Ne)=>{let ze=xs({type:"vault",websocket:{url:Ne.url,token:"tok-solo",clientId:"CLT-SOLO"}}).subscribe("/never-arrives",()=>{},{mode:"wait"}),De=!1,Ve="";ze.then(()=>{},(Je)=>{De=!0,Ve=Je instanceof Error?Je.message:String(Je)}),await Qc(200),Xc();try{await ze}catch(Je){De=!0,Ve=Je instanceof Error?Je.message:String(Je)}e(De,"wait-mode subscribe must reject when the socket is disposed (no hang)"),e(Ve.toLowerCase().includes("disposed")||Ve.toLowerCase().includes("interrupted"),`rejection should explain the socket teardown (got: ${Ve})`)})}}]}}async function Pe(){let Be=performance.now(),{tests:Ne}=_(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Vault websocket transport",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{ACCEPT_JSON as kn,readJsonResponse as fo}from"@ionite/core";function yo(Be,Ne){return e(typeof Be==="string"&&Be.length>0,`${Ne} must be configured`),Be}function Ja(Be,Ne){return e(typeof Be==="object"&&Be!==null&&!Array.isArray(Be),`${Ne} must be an object`),Be}function ed(Be,Ne){return e(typeof Be==="boolean",`${Ne} must be a boolean`),Be}function zp(Be,Ne){return e(Array.isArray(Be),`${Ne} must be an array`),Be}function Dp(){return{"~standard":{validate:(Be)=>{if(typeof Be!=="object"||Be===null)return{issues:[{message:`Expected object, got ${typeof Be}`}]};let Ne=Be,Le=[];if(typeof Ne.kty!=="string")Le.push({message:"Expected kty to be string",path:["kty"]});if(typeof Ne.kid!=="string")Le.push({message:"Expected kid to be string",path:["kid"]});if(Le.length>0)return{issues:Le};return{value:Be}}}}}function td(){return{"~standard":{validate:(Be)=>{if(typeof Be!=="object"||Be===null)return{issues:[{message:`Expected object, got ${typeof Be}`}]};let Ne=Be,Le=[];if(typeof Ne.valid!=="boolean")Le.push({message:"Expected valid to be boolean",path:["valid"]});if(Le.length>0)return{issues:Le};return{value:Be}}}}}var sd={validatePath:"/api/workload/validate",jwksPath:"/api/workload/jwks"};async function Cr(Be){if(Be.validToken)return Be.validToken;return o(Ra(Be.ecvUrl,"Workload validation ecvUrl when validToken is absent"))}async function ad(Be,Ne){return s("Workload JWKS Endpoint",async()=>{let Le=await Ne(yo(Be.jwksPath,"jwksPath"),{headers:kn});e(Le.status===200,`Expected 200 OK, got ${Le.status}`);let ze=Ja(await fo(Le),"JWKS response"),De=zp(ze.keys,"JWKS keys"),Ve=Dp();for(let Je of De)r(Je,Ve);return{details:{keyCount:De.length,algorithms:De.map((Je)=>Ja(Je,"JWKS key").alg).filter((Je)=>typeof Je==="string")}}})}async function nd(Be,Ne,Le){return s("Workload Validate Endpoint",async()=>{let ze=await Ne(yo(Be.validatePath,"validatePath"),{method:"POST",headers:{...kn,Authorization:`Bearer ${Le}`}});e(ze.status===200,`Expected 200 OK for valid token, got ${ze.status}`);let De=Ja(await fo(ze),"Token validation response"),Ve=td();r(De,Ve);let Je=ed(De.valid,"Validation result valid");e(Je===!0,"Token should be valid");let Ge=new Set(["valid","expiresAt"]),We=Object.keys(De).filter((Ke)=>!Ge.has(Ke));return e(We.length===0,`Validate response leaked fields: ${We.join(", ")}`),{details:{valid:Je,expiresAt:De.expiresAt}}})}async function od(Be,Ne){return s("Workload Validate Endpoint (Invalid Token)",async()=>{let Le=await Ne(yo(Be.validatePath,"validatePath"),{method:"POST",headers:{...kn,Authorization:"Bearer invalid.token.here"}});e(Le.status===401,`Expected 401 Unauthorized for invalid token, got ${Le.status}`);let ze=Ja(await fo(Le),"Invalid token validation response"),De=td();r(ze,De);let Ve=ed(ze.valid,"Validation result valid");e(Ve===!1,"Invalid token should not validate"),e(ze.error==="invalid_token",`Expected generic 'invalid_token' error, got ${String(ze.error)}`);let Je=new Set(["valid","error"]),Ge=Object.keys(ze).filter((We)=>!Je.has(We));return e(Ge.length===0,`Invalid-token response leaked fields: ${Ge.join(", ")}`),{details:{valid:Ve,error:ze.error}}})}async function rd(Be,Ne){return s("Workload Validate Endpoint (No Auth)",async()=>{let Le=await Ne(yo(Be.validatePath,"validatePath"),{method:"POST",headers:kn});return e(Le.status===401,`Expected 401 Unauthorized for missing auth, got ${Le.status}`),{details:{status:Le.status,requiresAuth:!0}}})}async function id(Be,Ne){return s("Workload Authentication (Whoami)",async()=>{let ze=`${Ra(Be.ecvUrl,"Workload validation ecvUrl")}/api/whoami`,De=await globalThis.fetch(ze,{headers:{...kn,Authorization:`Bearer ${Ne}`}});if(De.status===404)return{details:{skipped:!0,reason:"ECV whoami endpoint not available"}};e(De.status===200,`Expected 200 OK, got ${De.status}`);let Ve=Ja(await fo(De),"Whoami response"),Je=Ja(Ve.workload,"Whoami workload");return{details:{workloadId:Je.workloadId,clientId:Je.clientId}}})}async function _e(Be){let Ne=performance.now(),Le={...sd,...Be},ze=a(Le),De=[];De.push(await ad(Le,ze)),De.push(await rd(Le,ze)),De.push(await od(Le,ze));let Ve=await Cr(Le);De.push(await nd(Le,ze,Ve)),De.push(await id(Le,Ve));let Je=performance.now()-Ne,Ge=De.filter((Fe)=>Fe.passed).length,We=De.filter((Fe)=>!Fe.passed).length,Ke=De.filter((Fe)=>Fe.details?.skipped).length;return{suite:"Workload",passed:We===0,tests:De,duration:Je,summary:{total:De.length,passed:Ge-Ke,failed:We,skipped:Ke}}}function j(Be){let Ne={...sd,...Be},Le=a(Ne);return[{name:"JWKS endpoint returns valid keys",fn:async()=>{let ze=await ad(Ne,Le);if(!ze.passed)throw Error(ze.error)}},{name:"validate endpoint rejects missing auth",fn:async()=>{let ze=await rd(Ne,Le);if(!ze.passed)throw Error(ze.error)}},{name:"validate endpoint rejects invalid tokens",fn:async()=>{let ze=await od(Ne,Le);if(!ze.passed)throw Error(ze.error)}},{name:"validate endpoint accepts valid token",fn:async()=>{let ze=await Cr(Ne),De=await nd(Ne,Le,ze);if(!De.passed)throw Error(De.error)}},{name:"whoami endpoint works with workload authentication",fn:async()=>{let ze=await Cr(Ne),De=await id(Ne,ze);if(!De.passed&&!De.details?.skipped)throw Error(De.error)}}]}var Wp="/api/ionite";function Kp(Be){let Ne=typeof Be.basePath==="function"?Be.basePath():Be.basePath;return Be.capabilitiezPath??`${Ne??Wp}/capabilitiez`}function Sn(Be){let Ne=typeof Be.ecv==="function"?Be.ecv():Be.ecv;if(!Ne)throw Error("ECV server handle is required");return Ne.baseUrl}function cd(Be,Ne){e(typeof Be==="object"&&Be!==null,`capabilities.${Ne} must be an object`);let Le=Be;if(e(typeof Le.module==="string",`capabilities.${Ne}.module must be a string`),e(typeof Le.active==="boolean",`capabilities.${Ne}.active must be a boolean`),e(typeof Le.ready==="boolean",`capabilities.${Ne}.ready must be a boolean`),e(typeof Le.testable==="boolean",`capabilities.${Ne}.testable must be a boolean`),Le.urls!==void 0)e(Array.isArray(Le.urls)&&Le.urls.every((ze)=>typeof ze==="string"),`capabilities.${Ne}.urls must be a string array when provided`);if(Le.reason!==void 0)e(typeof Le.reason==="string",`capabilities.${Ne}.reason must be a string when provided`);return Le}function Rr(Be,Ne="manifest"){if(Array.isArray(Be)){Be.forEach((ze,De)=>Rr(ze,`${Ne}[${De}]`));return}if(typeof Be!=="object"||Be===null)return;let Le=new Set(["apiKey","clientSecret","privateKey","remoteConfig","signingKey","token"]);for(let[ze,De]of Object.entries(Be))e(!Le.has(ze),`capabilitiez manifest must not expose sensitive field ${Ne}.${ze}`),Rr(De,`${Ne}.${ze}`)}async function J(Be){let Ne=a(Be),Le=await o(Sn(Be),q),ze=await Ne(Kp(Be),{headers:{...Vp,Authorization:`Bearer ${Le}`}});e(ze.status===200,`Expected 200 from capabilitiez endpoint, got ${ze.status}. Ensure RemoteConfig includes capabilitiez: true and workload auth is configured.`);let De=await Gp(ze);e(typeof De.sdk==="object"&&De.sdk!==null,"capabilitiez manifest must include sdk"),e(typeof De.sdk.version==="string","capabilitiez manifest sdk.version must be a string"),e(Number.isInteger(De.sdk.catalogVersion)&&De.sdk.catalogVersion>0,"capabilitiez manifest sdk.catalogVersion must be a positive integer"),e(typeof De.basePath==="string","capabilitiez manifest must include basePath:string"),e(typeof De.lfvCallbackTransport==="boolean","capabilitiez manifest must include lfvCallbackTransport:boolean"),e(typeof De.urls==="object"&&De.urls!==null,"capabilitiez manifest must include urls"),e(typeof De.capabilities==="object"&&De.capabilities!==null,"capabilitiez manifest must include capabilities");for(let Ve of Bp)cd(De.capabilities[Ve.key],Ve.key);for(let Ve of Object.keys(De.capabilities))cd(De.capabilities[Ve],Ve);return Rr(De),De}function Fp(Be){return Be.toLowerCase().replace(/§\d+(?:\.\d+)*/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function Tn(Be,Ne,Le,ze,De=`${Be}.${Fp(Le)}`){return{id:De,unit:Be,suite:Ne,name:Le,title:`ECV(${De}) ${Le}`,run:ze}}function wo(Be,Ne){return e(Be!==void 0,`${Ne} requires the capabilitiez manifest`),Be}function vo(Be){let Ne;return(Le)=>(Ne??=Be(Le),Ne)}function Jt(Be,Ne,Le,ze){for(let De of ze)Be.push(Tn(Ne,Le,De.name,async()=>De.fn()))}function ss(Be,Ne,Le,ze,De){ze.forEach((Ve,Je)=>{Be.push(Tn(Ne,Le,Ve.name,async(Ge)=>{let We=De(wo(Ge,Ne))[Je];e(We!==void 0,`${Ne} test "${Ve.name}" could not be resolved`),await We.fn()}))})}function Hp(Be){Jt(Be,"sdk.config-retry","SDK / Config retry policy",L()),Jt(Be,"sdk.config-lifecycle","SDK / Config lifecycle",M()),Jt(Be,"sdk.config-boundary","SDK / Config boundary",N()),Jt(Be,"sdk.describe","SDK / describe()",fi().tests),Jt(Be,"sdk.harness","SDK / Harness",z()),Jt(Be,"sdk.authz-closure","SDK / Authz closure",u().tests),Jt(Be,"sdk.claims-entitlements","SDK / Claims entitlements",pi()),Jt(Be,"sdk.tenant-access","SDK / Tenant access",I().tests),Jt(Be,"sdk.lfv-callbacks","SDK / LFV callbacks",g().tests),Jt(Be,"sdk.secrets-hardening","SDK / Secrets hardening",S().tests),Jt(Be,"sdk.vault-coordination","SDK / Vault coordination",P().tests),Jt(Be,"sdk.vault-websocket","SDK / Vault WebSocket",_().tests),Jt(Be,"sdk.iam-delta","SDK / IAM delta",y().tests),Jt(Be,"sdk.iam-principals","SDK / IAM principals",w().tests),Jt(Be,"sdk.kms","SDK / KMS",v().tests),Jt(Be,"sdk.otel","SDK / OpenTelemetry",b().tests),Jt(Be,"sdk.protocol-surface","SDK / Protocol surface",k().tests),Jt(Be,"sdk.sso-id-token-verification","SDK / SSO ID token verification",m().tests),Jt(Be,"sdk.sso-control-contract","SDK / SSO control contract",R().tests),Jt(Be,"sdk.sso-token-client-auth","SDK / SSO token client auth",A().tests),Jt(Be,"sdk.sso-session-window","SDK / SSO session window",E().tests),Jt(Be,"sdk.display-cookie","SDK / display cookies",x().tests),Jt(Be,"sdk.session-entries","SDK / session entries",T().tests)}function G(Be){return[Tn("meta.run","ECV metadata",Ni(Jp).replace(/^ECV\(meta\.run\)\s*/,""),async(Ne,Le)=>Li(wo(Ne,"meta.run"),Be(),Le),"meta.run"),Tn("meta.parity","ECV metadata","declared claims match runtime-testable capabilities",async(Ne,Le)=>zi(wo(Ne,"meta.parity"),Be(),Le),"meta.parity")]}function Yp(Be,Ne,Le={}){let ze=[],De=C({...Be}),Ve=vo((Je)=>C({...Be,loginPath:Je.urls.sso.loginUrl,callbackPath:Je.urls.sso.callbackUrl,userPath:Je.urls.sso.userUrl,logoutPath:Je.urls.sso.logoutUrl,backChannelLogoutPath:Je.urls.sso.logoutBackChannelUrl,tokenPath:Je.urls.sso.loginUrl.replace(/\/login$/,"/token"),refreshPath:Je.urls.sso.refreshUrl}));if(Ne==="sso.login"){if(ss(ze,"sso.login","SSO / Core login",De.tests,(Je)=>Ve(Je).tests),Le.expectedCallbackErrorPattern){let Je=Le.expectedCallbackErrorPattern;ss(ze,"sso.login","SSO / Core login",De.ext.createCallbackErrorSurfacedTests(Je),(Ge)=>Ve(Ge).ext.createCallbackErrorSurfacedTests(Je))}}if(Ne==="sso.jit")ss(ze,"sso.jit","SSO / JIT",De.ext.createJITTests(),(Je)=>Ve(Je).ext.createJITTests());if(Ne==="sso.claims_authz"){let Je=Le.ssoClaimsAuthz;e(Je!==void 0,"sso.claims_authz requires a permissionProbePath and group");let Ge=Xo(Be,{loginPath:"",permissionProbePath:Je.permissionProbePath},Je.group);ss(ze,"sso.claims_authz","SSO / Claims entitlements",Ge,(We)=>(Vn(We,"sso.claims_authz"),Xo(Be,{loginPath:We.urls.sso.loginUrl,permissionProbePath:Je.permissionProbePath},Je.group)))}if(Ne==="sso.logout")ss(ze,"sso.logout","SSO / Logout",De.ext.createLogoutTests(),(Je)=>Ve(Je).ext.createLogoutTests());if(Ne==="sso.back_channel_logout")ss(ze,"sso.back_channel_logout","SSO / Back-channel logout",De.ext.createBackChannelLogoutTests(),(Je)=>Ve(Je).ext.createBackChannelLogoutTests());return ze}function Xp(Be,Ne,Le){let ze=[],De=h({...Be}),Ve=vo((Je)=>h({...Be,customerPath:Je.urls.ciam.customerUrl,magicLinkPath:Je.urls.ciam.magicLinkUrl,magicLinkLoginPath:Je.urls.ciam.magicLinkLoginUrl,logoutPath:Je.urls.ciam.logoutUrl,customerProvidersPath:Je.urls.ciam.customerProvidersUrl,customerLoginPath:Je.urls.ciam.customerLoginUrl}));if(Ne==="ciam.claims_authz"){let Je=Le.ciamClaimsAuthz;e(Je!==void 0,"ciam.claims_authz requires a permissionProbePath and group");let Ge=Zo(Be,{customerLoginPath:"",customerProvidersPath:"",permissionProbePath:Je.permissionProbePath},Je.group);ss(ze,"ciam.claims_authz","CIAM / Claims entitlements",Ge,(We)=>(Vn(We,"ciam.claims_authz"),Zo(Be,{customerLoginPath:We.urls.ciam.customerLoginUrl,customerProvidersPath:We.urls.ciam.customerProvidersUrl,permissionProbePath:Je.permissionProbePath},Je.group)))}if(Ne==="ciam.customer")ss(ze,"ciam.customer","CIAM / Customer",De.ext.createCustomerTests(),(Je)=>Ve(Je).ext.createCustomerTests());if(Ne==="ciam.magic_link")ss(ze,"ciam.magic_link","CIAM / Magic link",De.ext.createMagicLinkTests(),(Je)=>Ve(Je).ext.createMagicLinkTests());if(Ne==="ciam.oidc_login")ss(ze,"ciam.oidc_login","CIAM / OIDC login",De.ext.createOidcLoginTests(),(Je)=>Ve(Je).ext.createOidcLoginTests());if(Ne==="ciam.logout")ss(ze,"ciam.logout","CIAM / Logout",De.ext.createLogoutTests(),(Je)=>Ve(Je).ext.createLogoutTests());return ze}function Zp(Be,Ne){let Le=[],ze=()=>Sn(Be),De=f({...Be,ecvUrl:ze,ion:Be.ion}),Ve=vo((Je)=>f({...Be,ecvUrl:ze,scimPath:Je.urls.iam.basePath,groupsInboundPath:Je.urls.iam.groupsUrl,ion:Be.ion}));if(Ne==="iam.users")ss(Le,"iam.users","IAM / Users",De.tests,(Je)=>Ve(Je).tests);if(Ne==="iam.workloads"||Ne==="iam.customers")return Le;if(Ne==="iam.groups")ss(Le,"iam.groups","IAM / Groups inbound",De.ext.createGroupsInboundTests(),(Je)=>Ve(Je).ext.createGroupsInboundTests(Je.capabilities["iam.groups.create"].testable));if(Ne==="iam.groups.create")ss(Le,"iam.groups.create","IAM / Groups outbound",De.ext.createGroupsOutboundTests(),(Je)=>Ve(Je).ext.createGroupsOutboundTests()),ss(Le,"iam.groups.create","IAM / IAM-sourced entitlements",De.ext.createGroupsInboundCreateTests(),(Je)=>Ve(Je).ext.createGroupsInboundCreateTests());return Le}function Qp(Be,Ne){let Le=[];if(Ne==="workload.incoming"){let ze=()=>Sn(Be),De=j({...Be,ecvUrl:ze}),Ve=vo((Je)=>j({...Be,ecvUrl:ze,validatePath:Je.urls.workload.validateUrl,jwksPath:Je.urls.workload.jwksUrl}));ss(Le,"workload.incoming","Workload / Incoming",De,Ve)}if(Ne==="workload.claims_authz"){let ze=Yo(Be,()=>Sn(Be),"");ss(Le,"workload.claims_authz","Workload / Claims entitlements",ze,(De)=>(Vn(De,"workload.claims_authz"),Yo(Be,()=>Sn(Be),De.urls.capabilitiezUrl)))}if(Ne==="workload.outgoing")Le.push(Tn("workload.outgoing","Workload / Outgoing","runtime capability is testable",async(ze)=>{let De=wo(ze,"workload.outgoing");e(De.capabilities["workload.outgoing"].testable,De.capabilities["workload.outgoing"].reason??"workload.outgoing is not testable")}));return Le}function em(Be,Ne,Le){let ze=[];if(Ne==="authz.oaa"){let De={ecv:Be.ecv,timeout:Be.timeout},Ve=p(De);ss(ze,"authz.oaa","Authz / OAA",Ve,()=>Ve)}if(Ne==="authz.access_requests"){let De=Le.accessRequests;e(De?.users!==void 0||De?.customers!==void 0,"authz.access_requests requires users and/or customers request, probe, subject, and target options");let Ve=()=>{let Ge=typeof Be.ecv==="function"?Be.ecv():Be.ecv;if(!Ge)throw Error("ECV server handle is required");return Ge},Je=zo(Be,Ve,{ssoLoginPath:"",customerLoginPath:"",customerProvidersPath:""},De);ss(ze,"authz.access_requests","Authz / Access requests",Je,(Ge)=>zo(Be,Ve,{ssoLoginPath:Ge.urls.sso.loginUrl,customerLoginPath:Ge.urls.ciam.customerLoginUrl,customerProvidersPath:Ge.urls.ciam.customerProvidersUrl},De))}return ze}function W(){let Be=[];return Hp(Be),Be}function K(Be,Ne={}){let Le=[],ze=B({baseUrl:Ne.baseUrl??Be.baseUrl,timeout:Be.timeout,skipTlsVerify:Be.skipTlsVerify,tenantPath:Ne.tenantPath,profile:Ne.profile,webhookPath:Ne.webhookPath,ecv:()=>{let De=typeof Be.ecv==="function"?Be.ecv():Be.ecv;return e(De!==void 0,"ecv.tenants() requires the ECV server handle returned by setup()"),De}});return Jt(Le,"tenant.management","Tenant / Management",ze),Le}function F(Be,Ne,Le={}){if(Ne.startsWith("sso."))return Yp(Be,Ne,Le);if(Ne.startsWith("ciam."))return Xp(Be,Ne,Le);if(Ne.startsWith("iam."))return Zp(Be,Ne);if(Ne.startsWith("workload."))return Qp(Be,Ne);return em(Be,Ne,Le)}import{GenericContainer as pd,Wait as sm}from"testcontainers";import{ACCEPT_JSON as dd,CONTENT_TYPE_JSON as tm,readJsonResponse as ld,unexpectedBodySummary as ud}from"@ionite/core";async function Ga(Be,Ne,Le){let ze=await fetch(`${Be.baseUrl}/__ecv${Ne}`,{...Le,headers:{...dd,Authorization:`Bearer ${Be.env.ION_VAULT_TOKEN}`,...Le?.body?tm:{},...Le?.headers}});if(!ze.ok){let De=await ud(ze);throw Error(`ECV control API ${Le?.method??"GET"} ${Ne} failed: ${ze.status}${De.excerpt?` ${De.excerpt}`:""}`)}if(ze.status===204)return;return await ld(ze)}function se(Be){return{...Be,reset:()=>Ga(Be,"/reset",{method:"POST"}),decideAccessRequest:(Ne,Le)=>Ga(Be,`/access-requests/${encodeURIComponent(Ne)}/decision`,{method:"POST",body:JSON.stringify({status:Le})}),setTestUser:(Ne)=>Ga(Be,"/test-user",{method:"PUT",body:JSON.stringify(Ne)}),mintTmHandle:async(Ne)=>(await Ga(Be,"/tm/handle",{method:"POST",body:JSON.stringify(Ne)})).handle,oaa:{...Be.oaa,pushes:()=>Ga(Be,"/oaa/pushes"),clear:()=>Ga(Be,"/oaa/pushes",{method:"DELETE"})}}}async function H(Be,Ne="local-ecv-token"){let Le=Be.replace(/\/$/,""),ze=await fetch(`${Le}/__ecv/info`,{headers:{...dd,Authorization:`Bearer ${Ne}`}});if(!ze.ok){let Je=await ud(ze);throw Error(`ECV control API GET /info failed: ${ze.status}${Je.excerpt?` ${Je.excerpt}`:""}`)}let De=await ld(ze),Ve=new URL(Le);return se({...De,baseUrl:Le,host:Ve.hostname,port:Number(Ve.port||(Ve.protocol==="https:"?443:80))})}var bo=3515;class Y{constructor(Be,Ne){this.container=Be,this.ecv=Ne}get handle(){return this.ecv}serialize(){let{reset:Be,decideAccessRequest:Ne,setTestUser:Le,mintTmHandle:ze,oaa:De,...Ve}=this.ecv,{pushes:Je,clear:Ge,...We}=De;return{...Ve,oaa:We}}async stop(){await this.container.stop()}}class O{constructor(Be={}){this.options=Be}async createContainer(){if(this.options.dockerfileContext)return pd.fromDockerfile(this.options.dockerfileContext,this.options.dockerfileName??"packages/ecv-server/Dockerfile").build(this.options.image??"ionite/ecv:local",{deleteOnExit:!0});return new pd(this.options.image??process.env.ECV_IMAGE??`quay.io/ionite/ecv:${i}`)}async start(){let Be=await this.createContainer();Be.withExposedPorts(bo).withEnvironment({HOST:"0.0.0.0",PORT:String(bo),...this.options.redisUrl?{REDIS_URL:this.options.redisUrl}:{}}).withWaitStrategy(sm.forHttp("/readyz",bo)).withStartupTimeout(this.options.startupTimeout??120000);let Ne=await Be.start(),Le=`http://${Ne.getHost()}:${Ne.getMappedPort(bo)}`,ze=await H(Le);return new Y(Ne,ze)}}import{GenericContainer as md,Wait as hd}from"testcontainers";async function je(Be={}){let Ne,Le,ze;try{let De=Be.redisUrl;if(!De)Ne=await new md(Be.redisImage??"redis:7-alpine").withExposedPorts(6379).withWaitStrategy(hd.forLogMessage(/Ready to accept connections/)).start(),De=`redis://${Ne.getHost()}:${Ne.getMappedPort(6379)}`;let Ve=Be.postgres?.url;if(Be.postgres&&!Ve){let Je=Be.postgres;Le=await new md(Je.image??"postgres:16-alpine").withEnvironment({POSTGRES_DB:Je.database,POSTGRES_USER:Je.user,POSTGRES_PASSWORD:Je.password}).withExposedPorts(5432).withWaitStrategy(hd.forLogMessage(/database system is ready to accept connections/)).start(),Ve=`postgres://${encodeURIComponent(Je.user)}:${encodeURIComponent(Je.password)}@${Le.getHost()}:${Le.getMappedPort(5432)}/${encodeURIComponent(Je.database)}`}return ze=await new O({...Be.ecv,redisUrl:De}).start(),{ecv:ze.serialize(),redisUrl:De,databaseUrl:Ve,async stop(){await Promise.allSettled([ze?.stop(),Le?.stop(),Ne?.stop()].filter(Boolean))}}}catch(De){throw await Promise.allSettled([ze?.stop(),Le?.stop(),Ne?.stop()].filter(Boolean)),De}}import*as Ka from"node:crypto";import{CONTENT_TYPE_JSON as _r,defaultLogger as wd}from"@ionite/core";import{CACHE_BOUNDS as pa,clearJwksCache as So,resolveCacheConfig as xr,sso as vd,verifyIdToken as gd}from"@ionite/core/server";import{createSessionReadCache as fd,InMemorySessionStore as Wa}from"@ionite/server";import{createValidators as bd}from"@ionite/valibot";var To="https://idp.ecv.local/realms/ecv",Co="ecv-oidc-client",jr="ion.sso";function Er(Be){let{publicKey:Ne,privateKey:Le}=Ka.generateKeyPairSync("rsa",{modulusLength:2048}),ze=Ne.export({format:"jwk"});return{privateKey:Le,kid:Be,jwk:{kty:ze.kty,n:ze.n,e:ze.e,kid:Be,use:"sig",alg:"RS256"}}}function Ar(Be){return Buffer.from(Be).toString("base64url")}function Ir(Be,Ne){let Le={alg:"RS256",typ:"JWT",kid:Be.kid},ze=`${Ar(JSON.stringify(Le))}.${Ar(JSON.stringify(Ne))}`,De=Ka.createSign("RSA-SHA256");return De.update(ze),`${ze}.${Ar(De.sign(Be.privateKey))}`}function xn(Be={}){let Ne=Math.floor(Date.now()/1000);return{iss:To,aud:Co,sub:"ecv-user",iat:Ne,exp:Ne+3600,...Be}}function yd(){return`https://jwks.ecv.local/${Ka.randomUUID()}`}function Cn(Be){let Ne={get:0,update:0,delete:0};return{store:{async create(ze){await Be.create(ze)},async get(ze){return Ne.get++,Be.get(ze)},async update(ze,De){Ne.update++,await Be.update(ze,De)},async delete(ze){Ne.delete++,await Be.delete(ze)}},counts:Ne}}function Rn(Be){let Ne=new Date(Date.now()+3600000);return{sid:Be,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:xn({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:Ne,createdAt:new Date}}function ko(Be){return new Request("https://app.ecv.local/",{headers:{cookie:`${jr}.session=${Be}`}})}function Pr(Be){let Ne=bd(),Le=vd(Ne.sso,wd,void 0,{sessionStore:Be,cookiesPrefix:jr},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!Le)throw Error("sso() returned undefined");return Le}function am(Be,Ne,Le){let ze=bd(),De=vd(ze.sso,wd,void 0,{authority:To,tokenUrl:Ne,authorizationUrl:"https://idp.ecv.local/authorize",clientId:Co,redirectUri:"https://app.ecv.local/callback",scope:"openid",jwksUri:Le,sessionStore:Be,cookiesPrefix:jr,cookiesSecure:!1},{sessionCache:{read:{enabled:!1,ttlMs:30000,maxEntries:1024}}});if(!De)throw Error("sso() returned undefined");return De}function nm(Be){return{sid:Be,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:xn({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com"}),expiresAt:new Date(Date.now()+86400000),createdAt:new Date}}async function om(Be,Ne,Le){let ze=globalThis.fetch,De=0;So(Be),globalThis.fetch=async(Ve,Je)=>{if((typeof Ve==="string"?Ve:Ve.toString())===Be)return De++,new Response(JSON.stringify({keys:Ne()}),{status:200,headers:_r});return ze(Ve,Je)};try{return await Le(()=>De)}finally{globalThis.fetch=ze,So(Be)}}function Dt(Be,Ne){if(!Be)throw Error(Ne)}function Js(Be,Ne,Le){if(Be!==Ne)throw Error(`${Le} (expected ${Ne}, got ${Be})`)}var rm=(Be)=>new Promise((Ne)=>setTimeout(Ne,Be));function ae(){return{tests:[{name:"SSO read path builds user from stored claims with NO JWKS fetch",fn:async()=>{let{store:Ne,counts:Le}=Cn(new Wa);await Ne.create(Rn("sid-read"));let ze=Pr(Ne),De=globalThis.fetch,Ve=0;globalThis.fetch=async(Je,Ge)=>(Ve++,De(Je,Ge));try{let Je=await ze.resolveUser(ko("sid-read"));Dt(Je.status==="authenticated",`expected authenticated, got ${Je.status}`),Dt(Je.value.id==="ecv-user",`expected id ecv-user, got ${String(Je.value.id)}`),Dt(Ve===0,`read path performed ${Ve} fetch(es); expected 0`),Js(Le.get,1,`expected 1 store.get, got ${Le.get}`)}finally{globalThis.fetch=De}}},{name:"authenticated SSO read path performs ZERO session-store writes (§7.5)",fn:async()=>{let{store:Ne,counts:Le}=Cn(new Wa);await Ne.create(Rn("sid-nowrite"));let ze=Pr(Ne);for(let De=0;De<3;De++){let Ve=await ze.resolveUser(ko("sid-nowrite"));Dt(Ve.status==="authenticated",`expected authenticated, got ${Ve.status}`)}Js(Le.update,0,`read path must not write (got ${Le.update} update(s))`),Js(Le.delete,0,`read path must not delete a live session (got ${Le.delete})`)}},{name:"SSO refresh rotates the sid, strips the old refresh_token, and never 503s (§1.2)",fn:async()=>{let Ne=Er("kid-rotate"),Le=yd(),ze="https://idp.ecv.local/token",De=new Wa,Ve=[],Je={async create(We){Ve.push(We.sid),await De.create(We)},get:(We)=>De.get(We),update:(We,Ke)=>De.update(We,Ke),delete:(We)=>De.delete(We)};await Je.create(nm("sid-old"));let Ge=globalThis.fetch;So(Le),globalThis.fetch=async(We,Ke)=>{let Fe=typeof We==="string"?We:We.toString();if(Fe===Le)return new Response(JSON.stringify({keys:[Ne.jwk]}),{status:200,headers:_r});if(Fe==="https://idp.ecv.local/token"){let He=Math.floor(Date.now()/1000),Xe=Ir(Ne,xn({preferred_username:"ecv-user",name:"ECV User",email:"ecv@example.com",iat:He}));return new Response(JSON.stringify({access_token:"new-access",id_token:Xe,token_type:"Bearer",refresh_token:"new-refresh",expires_in:3600,refresh_expires_in:86400,scope:"openid"}),{status:200,headers:_r})}return Ge(We,Ke)};try{let Ke=await am(Je,"https://idp.ecv.local/token",Le).resolveUser(ko("sid-old"));Dt(Ke.status==="authenticated",`an expired token with a valid refresh token must refresh, never 503 — got ${Ke.status}`);let Fe=Ve.find((Ze)=>Ze!=="sid-old");Dt(!!Fe,"refresh must rotate to a new sid (new record created)");let He=await Je.get(Fe);Dt(He?.tokens.access_token==="new-access","the rotated record holds the refreshed access token"),Dt(He?.tokens.refresh_token==="new-refresh","the rotated record holds the new refresh token");let Xe=await Je.get("sid-old");Dt(Xe!=null,"old record survives during the rotation grace window"),Dt(Xe?.tokens.refresh_token===void 0,"old record must have its refresh_token stripped during grace");let Ye=new Date(Xe?.expiresAt).getTime();Dt(Ye<=Date.now()+60000+2000,`old record window must shrink to the grace (<=~60s), got ${Ye-Date.now()}ms`)}finally{globalThis.fetch=Ge,So(Le)}}},{name:"resolveUser is memoized per Request object (single store round-trip)",fn:async()=>{let{store:Ne,counts:Le}=Cn(new Wa);await Ne.create(Rn("sid-memo"));let ze=Pr(Ne),De=ko("sid-memo");await Promise.all([ze.resolveUser(De),ze.resolveUser(De),ze.resolveUser(De)]),Js(Le.get,1,`same-request resolveUser should hit the store once, got ${Le.get}`)}},{name:"session read cache: repeat get is served locally; miss is not cached; update/delete invalidate",fn:async()=>{let{store:Ne,counts:Le}=Cn(new Wa);await Ne.create(Rn("sid-cache"));let ze=fd(Ne,{ttlMs:30000,maxEntries:1024});await ze.get("sid-cache"),await ze.get("sid-cache"),Js(Le.get,1,`second get within TTL should be local, underlying got ${Le.get}`),await ze.get("missing"),await ze.get("missing"),Js(Le.get,3,`misses must not be cached (expected 3 underlying gets, got ${Le.get})`),await ze.update("sid-cache",{expiresAt:new Date(Date.now()+3600000)}),await ze.get("sid-cache"),Js(Le.get,4,`get after update should re-read (expected 4, got ${Le.get})`),await ze.delete("sid-cache");let De=await ze.get("sid-cache");Dt(De===null,"session should be gone after delete"),Js(Le.get,5,`get after delete should re-read (expected 5, got ${Le.get})`)}},{name:"session read cache: entry expires after ttlMs and re-reads",fn:async()=>{let{store:Ne,counts:Le}=Cn(new Wa);await Ne.create(Rn("sid-ttl"));let ze=fd(Ne,{ttlMs:20,maxEntries:1024});await ze.get("sid-ttl"),Js(Le.get,1,`first get reaches store, got ${Le.get}`),await rm(35),await ze.get("sid-ttl"),Js(Le.get,2,`get after TTL expiry should re-read (expected 2, got ${Le.get})`)}},{name:"RemoteConfig.cache values are clamped, not honored, when out of range",fn:async()=>{let Ne=xr({jwks:{ttlSeconds:1,maxEntries:1},session:{read:{enabled:!0,ttlSeconds:0,maxEntries:1}}});Dt(Ne.jwks.ttlMs===pa.jwksTtlSeconds.min*1000,`jwks.ttl should clamp to min, got ${Ne.jwks.ttlMs}`),Dt(Ne.jwks.maxEntries===pa.jwksMaxEntries.min,"jwks.maxEntries should clamp to min"),Dt(Ne.session.read.ttlMs===pa.sessionReadTtlSeconds.min*1000,"session.read.ttl should clamp to min"),Dt(Ne.session.read.maxEntries===pa.sessionReadMaxEntries.min,"read.maxEntries clamp to min");let Le=xr({jwks:{ttlSeconds:1e7,maxEntries:1e6},session:{read:{enabled:!0,ttlSeconds:1e4,maxEntries:1e7}}});Dt(Le.jwks.ttlMs===pa.jwksTtlSeconds.max*1000,"jwks.ttl should clamp to max"),Dt(Le.jwks.maxEntries===pa.jwksMaxEntries.max,"jwks.maxEntries should clamp to max"),Dt(Le.session.read.ttlMs===pa.sessionReadTtlSeconds.max*1000,"session.read.ttl should clamp to max");let ze=xr(void 0);Dt(ze.jwks.ttlMs===pa.jwksTtlSeconds.default*1000,"jwks.ttl default"),Dt(ze.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 Ne=Er("kid-perf-1"),Le=Er("kid-perf-2"),ze=yd(),De=[Ne.jwk];await om(ze,()=>De,async(Ve)=>{for(let Ge=0;Ge<3;Ge++){let We=await gd({idToken:Ir(Ne,xn()),jwksUri:ze,issuer:To,audience:Co});Dt(We.sub==="ecv-user","repeated verify should resolve claims")}Dt(Ve()===1,`3 verifications should share one JWKS fetch, got ${Ve()}`),De=[Ne.jwk,Le.jwk];let Je=await gd({idToken:Ir(Le,xn()),jwksUri:ze,issuer:To,audience:Co});Dt(Je.sub==="ecv-user","rotated-key token must verify after refresh"),Dt(Ve()===2,`rotation should trigger exactly one refresh, got ${Ve()}`)})}}]}}async function Oe(){let Be=performance.now(),{tests:Ne}=ae(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Performance tuning",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{isStoreConflictError as Ur,silentLogger as Mt}from"@ionite/core";import{chunkedMget as im,createEvalshaRunner as cm,createReactiveRedis as kd,DEFAULT_LIST_LIMIT as dm,DEFAULT_MGET_CHUNK_SIZE as lm,encodeKeyId as um,MAX_LIST_LIMIT as Sd,RedisAccessAssignmentStore as pm,RedisAccessCatalogStore as Td,RedisAccessIndex as Us,RedisMagicLinkStore as Cd,RedisSessionStore as En,RedisUserStore as An,scriptSha1 as Or,supportsRunScript as In}from"@ionite/redis-core";var Ct="ecv:redis:";function as(){let Be=new Map,Ne=new Map,Le=new Map,ze={get:0,set:0,del:0,sadd:0,srem:0,smembers:0,mget:0,runScript:0},De={raw:Be,sets:Ne,ttls:Le,mgetCalls:0,commands:ze,roundTrips:0,resetCounts(){De.mgetCalls=0,De.roundTrips=0;for(let Ve of Object.keys(ze))ze[Ve]=0},async get(Ve){return ze.get+=1,De.roundTrips+=1,Be.has(Ve)?Be.get(Ve):null},async set(Ve,Je){ze.set+=1,De.roundTrips+=1,Be.set(Ve,Je),Le.delete(Ve)},async setex(Ve,Je,Ge){De.roundTrips+=1,Be.set(Ve,Ge),Le.set(Ve,Date.now()+Je*1000)},async del(Ve){ze.del+=1,De.roundTrips+=1,Be.delete(Ve),Ne.delete(Ve),Le.delete(Ve)},async ttl(Ve){if(!Be.has(Ve))return-2;let Je=Le.get(Ve);if(Je==null)return-1;return Math.max(0,Math.ceil((Je-Date.now())/1000))},async getdel(Ve){De.roundTrips+=1;let Je=Be.has(Ve)?Be.get(Ve):null;return Be.delete(Ve),Le.delete(Ve),Je},async sadd(Ve,...Je){ze.sadd+=1,De.roundTrips+=1;let Ge=Ne.get(Ve)??new Set;for(let We of Je)Ge.add(We);Ne.set(Ve,Ge)},async srem(Ve,...Je){ze.srem+=1,De.roundTrips+=1;let Ge=Ne.get(Ve);if(Ge)for(let We of Je)Ge.delete(We)},async smembers(Ve){return ze.smembers+=1,De.roundTrips+=1,Array.from(Ne.get(Ve)??[])},async mget(Ve){return ze.mget+=1,De.mgetCalls+=1,De.roundTrips+=1,Ve.map((Je)=>Be.has(Je)?Be.get(Je):null)}};return De}function Pn(){let Be=as(),Ne=(Ge)=>Be.sets.get(Ge)?.size??0,Le=(Ge,We)=>{let Ke=Be.sets.get(Ge)??new Set;Ke.add(We),Be.sets.set(Ge,Ke)},ze=(Ge,We)=>{Be.sets.get(Ge)?.delete(We)},De=(Ge)=>Array.from(Be.sets.get(Ge)??[]),Ve=(Ge)=>{Be.raw.set(Ge,"1")},Je=(Ge)=>{Be.raw.delete(Ge),Be.sets.delete(Ge)};return Be.runScript=async(Ge,We,Ke)=>{if(Be.commands.runScript+=1,Be.roundTrips+=1,Ge.includes("local C=tonumber(ARGV[2])")){let Fe=Ke[0],He=Number(Ke[1]),Xe=Number(Ke[2]),Ye=Number(Ke[3]),Ze=We[0],tt=We[1];for(let et=0;et<He;et+=1){let nt=2+et*4;Je(We[nt]),Je(We[nt+1]),Je(We[nt+2]),Je(We[nt+3])}let Qe=2+He*4;for(let et=0;et<Xe;et+=1)Je(We[Qe+et]);Je(Ze);let st=Qe+Xe;for(let et=0;et<Ye;et+=1){let nt=st+et*5,[dt,pt,ct,ft,ut]=[We[nt],We[nt+1],We[nt+2],We[nt+3],We[nt+4]],Rt=4+et*3,[jt,Ft,Os]=[Ke[Rt],Ke[Rt+1],Ke[Rt+2]];Le(pt,jt),Ve(ct),Le(ft,Os),Ve(ut),Le(dt,Ft),Le(Ze,Ft)}if(Ye===0)ze(tt,Fe);else Le(tt,Fe);return"ok"}if(Ge.includes("local A=tonumber(ARGV[2])")){let Fe=Ke[0],He=Number(Ke[1]),Xe=Number(Ke[2]),Ye=We[0],Ze=We[1];for(let st=0;st<He;st+=1){let et=2+st*5,[nt,dt,pt,ct,ft]=[We[et],We[et+1],We[et+2],We[et+3],We[et+4]],ut=3+st*3,[Rt,jt,Ft]=[Ke[ut],Ke[ut+1],Ke[ut+2]];Le(nt,Fe),Ve(dt),Le(pt,jt),Ve(ct),Le(Ye,Rt),Le(ft,Rt),Le(Ze,Ft)}let tt=2+He*5,Qe=3+He*3;for(let st=0;st<Xe;st+=1){let et=tt+st*5,[nt,dt,pt,ct,ft]=[We[et],We[et+1],We[et+2],We[et+3],We[et+4]],ut=Qe+st*2,[Rt,jt]=[Ke[ut],Ke[ut+1]];if(ze(nt,Fe),Ne(nt)===0){if(Je(nt),Je(dt),ze(ft,Rt),ze(pt,jt),Ne(pt)===0)Je(pt),Je(ct)}ze(Ye,Rt)}return"ok"}if(Ge.includes("local n=tonumber(ARGV[2])")){let Fe=Ke[0],He=Number(Ke[1]),Xe=We[0];for(let Ye=0;Ye<He;Ye+=1){let Ze=1+Ye*5,[tt,Qe,st,et,nt]=[We[Ze],We[Ze+1],We[Ze+2],We[Ze+3],We[Ze+4]],[dt,pt]=[Ke[2+Ye*2],Ke[3+Ye*2]];if(ze(tt,Fe),Ne(tt)===0){if(Je(tt),Je(Qe),ze(nt,dt),ze(st,pt),Ne(st)===0)Je(st),Je(et)}}return Je(Xe),"ok"}if(Ge.includes("local roots={}")){let Fe=Number(Ke[0]),He=We[0],Xe=[],Ye=new Set;for(let Ze=0;Ze<Fe;Ze+=1){let tt=1+Ze*4,[Qe,st,et,nt]=[We[tt],We[tt+1],We[tt+2],We[tt+3]];for(let dt of De(Qe))if(!Ye.has(dt))Ye.add(dt),Xe.push(dt);Je(Qe),Je(st),Je(et),Je(nt)}return Je(He),Xe}if(Ge.includes("for i=1,#KEYS do")){for(let Fe of We)Je(Fe);return"ok"}throw Error("authz scripted adapter: unsupported script")},Be}function _n(){let Be=as();return Be.runScript=async(Ne,Le,ze)=>{if(Ne.includes("return 'created'")){let De=Le[0],Ve=ze[0],Je=Be.raw.has(De)?Be.raw.get(De):null;if(Je!=null)return Je===Ve?"exists_identical":"conflict";if(ze.length>=2)await Be.setex(De,Number(ze[1]),Ve);else await Be.set(De,Ve);return"created"}if(Ne.includes("return 'retry'")){let De=Le[0],Ve=ze[0],Je=ze[1],Ge=Be.raw.has(De)?Be.raw.get(De):null;if(Ge==null)return"missing";if(Ge!==Ve)return"retry";if(ze.length>=3)await Be.setex(De,Number(ze[2]),Je);else{let We=Be.ttls.get(De);if(Be.raw.set(De,Je),We!=null)Be.ttls.set(De,We)}return"ok"}if(Ne.includes("return {1,ARGV[1]}")){let[De,Ve]=Le,[Je,Ge]=ze,We=Be.raw.get(De);if(We!==void 0)return[0,We];Be.raw.set(De,Je);let Ke=Be.sets.get(Ve)??new Set;return Ke.add(Ge),Be.sets.set(Ve,Ke),[1,Je]}throw Error("scripted memory adapter: unsupported script")},Be}function Fa(Be,Ne,Le){return{sid:Be,sub:`user-${Ne}`,user:{id:`user-${Ne}`},createdAt:new Date(0),...Le?{expiresAt:Le}:{}}}function ne(){return{tests:[{name:"corrupt JSON payloads are discarded (get -> undefined, list skips) instead of throwing (§1.27)",fn:async()=>{let Ne=as(),Le=new An({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.upsert({id:"u-ok",userName:"ok",name:"OK",email:"ok@example.test"}),Ne.raw.set(`${Ct}user:${um("u-bad")}`,"{ this is : not valid json");let ze=Ne.sets.get(`${Ct}user:ids`);e(ze!=null,"user id set should exist after the first upsert"),ze?.add("u-bad");let De=await Le.get("u-bad");t(De,void 0,"a corrupt record must read back as undefined, not throw");let Ve=await Le.get("u-ok");e(Ve?.id==="u-ok","a valid record beside a corrupt one must still read");let Je=await Le.list();t(Je.items.length,1,"list must skip the corrupt record and return only valid ones"),t(Je.items[0]?.id,"u-ok","the surviving record should be the valid one")}},{name:"RedisUserStore maintains the optional externalId secondary index",fn:async()=>{let Ne=new An({redis:as(),keyPrefix:Ct,logger:Mt});await Ne.upsert({id:"local-user",externalId:"enterprise-user",userName:"indexed@example.test",name:"Indexed User",email:"indexed@example.test"}),t((await Ne.getByExternalId("enterprise-user"))?.id,"local-user","externalId resolves the app-owned user id"),await Ne.upsert({id:"local-user",externalId:"enterprise-user-renamed",userName:"indexed@example.test",name:"Indexed User",email:"indexed@example.test"}),t(await Ne.getByExternalId("enterprise-user"),void 0,"renaming clears the stale index"),t((await Ne.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 Ne=as(),Le=new An({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=Sd*2+50;for(let Je=0;Je<ze;Je+=1)await Le.upsert({id:`u-${Je}`,userName:`u-${Je}`,name:`User ${Je}`,email:`u${Je}@example.test`});let De=await Le.list();t(De.total,ze,"total must reflect every matching row (before pagination)"),t(De.items.length,dm,"an omitted limit must default-cap the page");let Ve=await Le.list({limit:ze});t(Ve.items.length,Sd,"an oversized explicit limit must clamp to the max page"),t(Ve.total,ze,"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 Ne=Array.from({length:5000},(Ve,Je)=>`k${Je}`),Le=0,ze=0,De=await im(Ne,async(Ve)=>(ze+=1,Le=Math.max(Le,Ve.length),Ve.map((Je)=>Number(Je.slice(1))%2===0?Je:null)));e(ze>1,"a 5000-key MGET must be split into multiple bounded batches"),e(Le<=lm,`each MGET batch must stay within the chunk size (saw ${Le})`),t(De.length,Ne.length,"chunked result length must match input length"),t(De[0],"k0","order must be preserved across chunks (first even key)"),t(De[1],null,"missing values must normalize to null (first odd key)"),t(De[4998],"k4998","last even key preserved"),t(De[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 Ne=as(),Le=new En({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=Fa("s1","a");await Le.create(ze),await Le.create(Fa("s1","a"));let De;try{await Le.create(Fa("s1","b"))}catch(Ke){De=Ke}e(Ur(De),"a different value at the same sid must throw a typed StoreConflictError");let Ve=new Cd({redis:Ne,keyPrefix:Ct,logger:Mt}),Je={id:"c1",userName:"c1"},Ge=new Date(Date.now()+60000);await Ve.create("tok",Je,Ge);let We;try{await Ve.create("tok",{id:"c2",userName:"c2"},Ge)}catch(Ke){We=Ke}e(Ur(We),"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 Ne=as(),Le=new An({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.upsert({id:"u1",userName:"alice",name:"Alice",email:"a@example.test"}),await Le.upsert({id:"u1",userName:"alice",name:"Alice II",email:"a@example.test"});let ze;try{await Le.upsert({id:"u2",userName:"alice",name:"Mallory",email:"m@example.test"})}catch(De){ze=De}e(Ur(ze),"claiming an in-use userName must throw a typed conflict"),t(await Le.get("u2"),void 0,"a conflicting upsert must not persist the record (checks-first)"),t((await Le.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 Ne=as(),Le=new En({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.create(Fa("s1","a"));let ze=new Date(Date.now()+1e6);await Le.update("s1",{expiresAt:ze,tenantId:"tnt-1"});let De=await Le.get("s1");t(new Date(De?.expiresAt).getTime(),ze.getTime(),"the patched field is applied"),t(De?.createdAt?.getTime(),0,"unpatched fields are preserved (shallow merge, no clobber)");let Ve=!1;try{await Le.update("missing",{tenantId:"x"})}catch{Ve=!0}e(Ve,"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 Ne=_n(),Le=new En({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.create(Fa("s-ttl","ttl",new Date(Date.now()+1e6)));let ze=Array.from(Ne.raw.keys()).find((Ge)=>Ge.includes("session"));e(!!ze,"a session key must exist after create");let De=await Ne.ttl?.(ze)??-1;e(De>0&&De<=1000,`create must set a bounded TTL from expiresAt (got ${De})`),await Le.update("s-ttl",{tenantId:"tnt"});let Ve=await Ne.ttl?.(ze)??-1;e(Ve>0,`an update without expiresAt must KEEPTTL (got ${Ve})`),await Le.update("s-ttl",{expiresAt:new Date(Date.now()+60000)});let Je=await Ne.ttl?.(ze)??-1;e(Je>0&&Je<=60,`grace re-TTL must shrink the key TTL to the grace (got ${Je})`)}},{name:"session create with no runScript (fallback path) still applies a TTL via setex (§1.3)",fn:async()=>{let Ne=as();await new En({redis:Ne,keyPrefix:Ct,logger:Mt}).create(Fa("s-ttl-fb","fb",new Date(Date.now()+500000)));let ze=Array.from(Ne.raw.keys()).find((Ve)=>Ve.includes("session"));e(!!ze,"a session key must exist after fallback create");let De=await Ne.ttl?.(ze)??-1;e(De>0&&De<=500,`fallback create must setex a bounded TTL (got ${De})`)}},{name:"hasAll/hasAny evaluate many targets with a single batched MGET (§7.4)",fn:async()=>{let Ne=as(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=[{subject:"u1",subjectType:"user",targetType:"permission",target:"p0"}];await Le.materialize("root1",ze);let De=Array.from({length:50},(Ve,Je)=>({targetType:"permission",target:`p${Je}`,resource:`r${Je}`}));Ne.mgetCalls=0,await Le.hasAll("u1",De,"user"),t(Ne.mgetCalls,1,"hasAll over 50 targets must issue exactly one batched MGET"),Ne.mgetCalls=0,await Le.hasAny("u1",De,"user"),t(Ne.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 Ne=as(),Le=new pm({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.grant({subject:"u1",subjectType:"user",targetType:"role",target:"r1"}),await Le.grant({subject:"u1",subjectType:"user",targetType:"role",target:"r2"});let ze=await Le.list();t(ze.total,2,"both active grants are counted"),await Le.revoke({subject:"u1",subjectType:"user",targetType:"role",target:"r1"});let De=await Le.list();t(De.total,1,"revoke removes the row from the id set so the total is accurate"),t(De.items.length,1,"only the surviving grant is returned"),t(De.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 Ne=as(),Le=new An({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=["u3","u1","u5","u2","u4"];for(let Ke of ze)await Le.upsert({id:Ke,userName:`${Ke}@example.com`});let De=await Le.list({start:0,limit:2}),Ve=await Le.list({start:2,limit:2}),Je=await Le.list({start:4,limit:2});t(De.total,5,"total reflects the full set on every page");let Ge=[...De.items,...Ve.items,...Je.items].map((Ke)=>Ke.id);t(Ge.join(","),"u1,u2,u3,u4,u5","pages are disjoint, complete, and in stable ascending id order");let We=await Le.list({start:0,limit:2});t(We.items.map((Ke)=>Ke.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 Le=kd({source:{async subscribe(){return()=>{}}},path:"redis/main",createClient:async()=>({adapter:as()})});await Le.close();let ze=!1;try{await Le.ready(50)}catch{ze=!0}e(ze,"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 Ne=_n(),ze=kd({source:{async subscribe(De,Ve){return Ve({value:{},metadata:{version:1}}),()=>{}}},path:"redis/main",createClient:async()=>({adapter:Ne})});await ze.ready(1000),e(In(ze),"reactive adapter must forward runScript once the client supports it"),e(typeof ze.getdel==="function","reactive adapter must forward getdel once the client supports it"),await ze.set(`${Ct}fwd:k`,"v"),t(await ze.getdel?.(`${Ct}fwd:k`),"v","forwarded getdel returns and deletes the value"),t(await ze.get(`${Ct}fwd:k`),null,"forwarded getdel deleted the key"),await ze.close()}},{name:"single-use consume over runScript/getdel yields exactly one winner (§10.1)",fn:async()=>{let Ne=_n();e(In(Ne),"scripted adapter must advertise runScript for this test");let Le=new Cd({redis:Ne,keyPrefix:Ct,logger:Mt}),ze={id:"c1",userName:"c1"};await Le.create("one-time",ze,new Date(Date.now()+60000));let Ve=(await Promise.all(Array.from({length:8},()=>Le.getAndDelete("one-time")))).filter((Je)=>Je!=null);t(Ve.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 Ne=_n();e(In(Ne),"scripted adapter must advertise runScript for this test");let Le=new En({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=Number.MAX_SAFE_INTEGER,De={sid:"s-merge",sub:"user-merge",user:{id:"user-merge"},claims:{groups:[],seq:ze},createdAt:new Date(0),expiresAt:new Date(Date.now()+1e6)};await Le.create(De),await Le.update("s-merge",{tenantId:"tnt-merge"});let Ve=await Le.get("s-merge");e(Ve!=null,"session must exist after update"),t(Ve?.tenantId,"tnt-merge","the patched field is applied"),e(Array.isArray(Ve?.claims.groups),"an empty-array claim must stay an array (not become {})"),t((Ve?.claims.groups).length,0,"the empty-array claim stays empty"),t(Ve?.claims.seq,ze,"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 Ne=as(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.materialize("root-a",[{subject:"u",subjectType:"user",targetType:"permission",target:"p",resource:"x:y"}]),e((await Le.has("u","permission","p","x:y","user")).allow,"the materialized edge (target p, resource x:y) must be allowed"),e(!(await Le.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 Ne=as(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.setSubjectDisabled("team:a","user",!0),e(await Le.isSubjectDisabled("team:a","user"),"the disabled marker round-trips for a `:`-bearing id"),e(!await Le.isSubjectDisabled("team","user"),"a prefix subject id must not alias another subject’s disabled marker"),e(!await Le.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 Ne=_n(),Le=new Td({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=Le.groups.insertIfAbsent({id:"code-admin",name:"Code Admin",description:"Code presentation",assignableSubjectTypes:["user"],source:"code",roles:["admin"]}),De=Le.groups.insertIfAbsent({id:"code-admin",name:"IAM Renamed Admin",description:"IAM-owned presentation",assignableSubjectTypes:["user"],source:"code",providerId:"provider-42",requestUrl:"https://iam.example.test/request/provider-42",roles:["must-not-win"]});await Promise.all([ze.ready(),De.ready()]);let Ve=await Le.groups.get("code-admin");e(Ve!==void 0,"one durable catalog row is installed"),t(ze.name,Ve?.name,"the first caller receives the winning row"),t(De.name,Ve?.name,"the racing caller converges on the same winning row"),t((await Le.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 Ne=new Td({redis:as(),keyPrefix:Ct,logger:Mt}),Le=0,ze=Ne.afterChange?.(()=>{Le+=1}),De={id:"stable-group",name:"Stable Group",description:"No-op write proof",requestable:!0,assignableSubjectTypes:["user"],first:1,second:2};await Ne.groups.set(De).ready();let Ve={requestable:!0,description:"No-op write proof",name:"Stable Group",id:"stable-group",assignableSubjectTypes:["user"],second:2,first:1};await Ne.groups.set(Ve).ready(),ze?.(),t(Le,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 Ne=as(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt}),ze="x".repeat(1e4);await Le.materialize("root-huge",[{subject:ze,subjectType:"user",targetType:"permission",target:ze}]);for(let De of Ne.raw.keys())e(De.length<512,`key must stay bounded even for a 10k-char id (saw ${De.length})`),e(!De.includes(ze),"the raw oversized id must never appear verbatim in a key");for(let De of Ne.sets.keys())e(De.length<512,`set key must stay bounded even for a 10k-char id (saw ${De.length})`)}},{name:"Lua teardown: removeByRoot of a 1000-edge root is O(1) client round trips, not O(edges) (§7.2)",fn:async()=>{let Ne=Pn();e(In(Ne),"scripted authz adapter must advertise runScript for the Lua path");let Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=Array.from({length:1000},(De,Ve)=>({subject:"big",subjectType:"user",targetType:"permission",target:`p${Ve}`}));await Le.materialize("root-big",ze),e((await Le.hasPermission("big","p0",void 0,"user")).allow,"a materialized edge resolves before teardown"),e((await Le.hasPermission("big","p999",void 0,"user")).allow,"the last materialized edge resolves too"),Ne.resetCounts(),await Le.removeByRoot("root-big"),t(Ne.commands.smembers,1,"teardown reads the root descriptor set with exactly one SMEMBERS"),t(Ne.commands.runScript,1,"teardown applies every edge removal in exactly one runScript"),e(Ne.roundTrips<=3,`a 1000-edge teardown must be O(1) client round trips, not O(edges) (saw ${Ne.roundTrips})`),e(!(await Le.hasPermission("big","p0",void 0,"user")).allow,"edges are gone after teardown"),e(!(await Le.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 Ne=Pn(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=[{subject:"u",subjectType:"user",targetType:"role",target:"a"},{subject:"u",subjectType:"user",targetType:"role",target:"b"}];await Le.materialize("root-x",ze),Ne.resetCounts(),await Le.materialize("root-x",ze),t(Ne.commands.runScript,0,"an empty diff must not invoke the write script at all"),e((await Le.hasRole("u","a",void 0,"user")).allow,"edge a stays present across a no-op re-materialize"),e((await Le.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 Ne=Pn(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.materialize("root-old",[{subject:"swap",subjectType:"user",targetType:"permission",target:"old",rootId:"root-old"}]),Ne.resetCounts(),await Le.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"}]),t(Ne.commands.runScript,1,"the complete closure replacement uses exactly one atomic script"),e(!(await Le.hasPermission("swap","old",void 0,"user")).allow,"the old closure is removed at the swap"),e((await Le.hasPermission("swap","new",void 0,"user")).allow,"the new closure is installed"),e((await Le.hasPermission("swap","shared",void 0,"user")).allow,"overlapping roots preserve their shared edge"),await Le.removeByRoot("root-new"),e((await Le.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 Ne=as(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.materialize("root-old",[{subject:"safe",subjectType:"user",targetType:"permission",target:"old",rootId:"root-old"}]);let ze=!1;try{await Le.replaceSubject("safe","user",[])}catch{ze=!0}e(ze,"replaceSubject requires runScript instead of exposing a non-atomic fallback"),e((await Le.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 Ne=Pn(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.materialize("root-y",[{subject:"u",subjectType:"user",targetType:"role",target:"keep"},{subject:"u",subjectType:"user",targetType:"role",target:"drop"}]),await Le.materialize("root-y",[{subject:"u",subjectType:"user",targetType:"role",target:"keep"}]),e((await Le.hasRole("u","keep",void 0,"user")).allow,"the unchanged edge survives a sibling removal"),e(!(await Le.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 Ne=Pn(),Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt});await Le.setSubjectDisabled("u","user",!0),await Le.materialize("root-z",[{subject:"u",subjectType:"user",targetType:"role",target:"a"}]),await Le.removeByRoot("root-z"),e(await Le.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 Ne=as();e(!In(Ne),"this test intends to exercise the no-runScript fallback");let Le=new Us({redis:Ne,keyPrefix:Ct,logger:Mt}),ze=Array.from({length:100},(De,Ve)=>({subject:"f",subjectType:"user",targetType:"permission",target:`p${Ve}`}));await Le.materialize("root-fb",ze),Ne.resetCounts(),await Le.removeByRoot("root-fb"),e(Ne.roundTrips<=1200,`fallback teardown must be O(edges) with a small constant (saw ${Ne.roundTrips})`),e(Ne.roundTrips<1e4,`fallback teardown must not be quadratic in edges (saw ${Ne.roundTrips})`),e(!(await Le.hasPermission("f","p0",void 0,"user")).allow,"fallback teardown removes edges"),e(!(await Le.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 Ne=0,Le=0,ze=new Set,Ve=cm({async eval(We,Ke,Fe){return Ne+=1,ze.add(Or(We)),"ok"},async evalsha(We,Ke,Fe){if(Le+=1,!ze.has(We))throw Error("NOSCRIPT No matching script. Please use EVAL.");return"ok"}}),Je=`redis.call('SET', KEYS[1], ARGV[1])
|
|
4
|
+
return 'ok'`;await Ve(Je,["k"],["v1"]),t(Le,1,"the runner always tries EVALSHA first"),t(Ne,1,"a cold NOSCRIPT falls back to EVAL exactly once"),await Ve(Je,["k"],["v2"]),await Ve(Je,["k"],["v3"]),t(Ne,1,"a stable script body never re-transmits the source after the first prime"),t(Le,3,"steady-state calls all go over EVALSHA (the 40-byte sha)");let Ge=`redis.call('DEL', KEYS[1])
|
|
5
|
+
return 'ok'`;e(Or(Je)!==Or(Ge),"distinct script bodies hash to distinct shas")}}]}}async function Ue(){let Be=performance.now(),{tests:Ne}=ne(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Redis stores",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{RedisTenantStore as jn}from"@ionite/redis-core";import{InMemoryAccessCatalogStore as mm,InMemoryTenantStore as Ro,ionite as hm,silentLogger as Rd}from"@ionite/server";import{createValidators as gm}from"@ionite/valibot";import{LRUCache as xo}from"lru-cache";function fm(Be){return{async load(){return Be},async subscribe(){return},secret:{}}}function Ta(Be,Ne=Be){return{id:Be,companyId:`co-${Be}`,companyName:Ne,environmentType:"DEV",status:"completed",configLocator:{type:"vault",vaultPath:`secret/data/tenants/${Be}`},createdAt:new Date,updatedAt:new Date}}function Eo(Be,Ne){return{close:Ne,__marker:Be}}function Ca(Be){return Be?.__marker}function xd(){let Be=new Map,Ne=new Map;return{async get(Le){return Be.has(Le)?Be.get(Le):null},async set(Le,ze){Be.set(Le,ze)},async del(Le){Be.delete(Le)},async mget(Le){return Le.map((ze)=>Be.has(ze)?Be.get(ze):null)},async sadd(Le,...ze){let De=Ne.get(Le)??new Set;for(let Ve of ze)De.add(Ve);return Ne.set(Le,De),ze.length},async srem(Le,...ze){let De=Ne.get(Le);if(!De)return 0;let Ve=0;for(let Je of ze)if(De.delete(Je))Ve+=1;return Ve},async smembers(Le){return Array.from(Ne.get(Le)??[])}}}function oe(){return{tests:[{name:"getIon() without an ionCache throws a guidance error — the SDK never constructs runtimes (§7.4)",fn:async()=>{let Ne=new Ro;await Ne.upsert(Ta("t1"));let Le;try{await Ne.getIon("t1")}catch(ze){Le=ze.message}e(Le!==void 0,"getIon() must throw when no ionCache is configured"),e(/ionCache/.test(Le??""),"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 Ne={constructions:0,closeCount:0},Le,ze=new xo({max:50,dispose:(Je)=>Je.close(),fetchMethod:async(Je)=>{let Ge=await Le.get(Je);if(!Ge)return;return await Promise.resolve(),Ne.constructions+=1,Eo(Ge.companyName,()=>{Ne.closeCount+=1})}});Le=new Ro({ionCache:ze}),await Le.upsert(Ta("t1"));let[De,Ve]=await Promise.all([Le.getIon("t1"),Le.getIon("t1")]);t(Ne.constructions,1,"concurrent getIon must coalesce into a single construction"),e(De===Ve,"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 Ne={constructions:0,closeCount:0},Le,ze=new xo({max:50,dispose:(Je)=>Je.close(),fetchMethod:async(Je)=>{let Ge=await Le.get(Je);if(!Ge)return;return Ne.constructions+=1,Eo(Ge.companyName,()=>{Ne.closeCount+=1})}});Le=new Ro({ionCache:ze}),await Le.upsert(Ta("t1","v1"));let De=await Le.getIon("t1");t(Ca(De),"v1","first build reflects the v1 record"),t(Ne.constructions,1,"one construction after the first getIon"),await Le.upsert(Ta("t1","v2")),t(Ne.closeCount,1,"upsert must close the previously cached runtime");let Ve=await Le.getIon("t1");t(Ca(Ve),"v2","the next getIon rebuilds from the updated record"),t(Ne.constructions,2,"a fresh construction follows invalidation")}},{name:"delete closes the cached runtime (§7.4)",fn:async()=>{let Ne={constructions:0,closeCount:0},Le,ze=new xo({max:50,dispose:(De)=>De.close(),fetchMethod:async(De)=>{let Ve=await Le.get(De);if(!Ve)return;return Ne.constructions+=1,Eo(Ve.companyName,()=>{Ne.closeCount+=1})}});Le=new Ro({ionCache:ze}),await Le.upsert(Ta("t1")),await Le.getIon("t1"),t(Ne.closeCount,0,"no close before deletion"),await Le.delete("t1"),t(Ne.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 Ne=new mm,Le=0,ze=0,De=Ne.afterChange.bind(Ne);Ne.afterChange=(We)=>{Le+=1;let Ke=De((Fe)=>(ze+=1,We(Fe)));return()=>{Le-=1,Ke()}};let Ve=(We)=>hm(fm({id:We}),{validators:gm(),stores:{catalogStore:Ne}}),Je=Ve("batch6-authz-a"),Ge=Ve("batch6-authz-b");await Je.ready(1000),await Ge.ready(1000),t(Le,2,"both instances subscribe exactly one catalog listener"),ze=0,Ne.groups.set({id:"g1"}),t(ze,2,"both instances react to the change while open"),Je.close(),t(Le,1,"ion.close() detaches the closed instance’s catalog listener"),ze=0,Ne.groups.set({id:"g2"}),t(ze,1,"exactly one reconcile/projection fires after the other instance is closed"),Ge.close(),t(Le,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 Ne=xd(),Le=`ion:t:batch6-node:${crypto.randomUUID()}:`,ze=()=>{let Je,Ge=new xo({max:50,dispose:(We)=>We.close(),fetchMethod:async(We)=>{let Ke=await Je.get(We);if(!Ke)return;return Eo(Ke.companyName,()=>{})}});return Je=new jn({redis:Ne,keyPrefix:Le,logger:Rd,ionCache:Ge}),{store:Je,ionCache:Ge}},De=ze(),Ve=ze();await De.store.upsert(Ta("t","v1")),t(Ca(await De.store.getIon("t")),"v1","node A builds from v1"),t(Ca(await Ve.store.getIon("t")),"v1","node B builds from v1"),await De.store.upsert(Ta("t","v2")),t(Ca(await De.store.getIon("t")),"v2","node A rebuilds from the new record"),t(Ca(await Ve.store.getIon("t")),"v1","node B keeps serving the old runtime until its own cache evicts (documented staleness)"),Ve.ionCache.delete("t"),t(Ca(await Ve.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 Ne=xd(),Le=[],ze={...Rd,warn:(Ge)=>{Le.push(String(Ge))}},De=`ion:t:batch6-dup:${crypto.randomUUID()}:`,Ve={redis:Ne,keyPrefix:De,logger:ze};new jn(Ve),new jn(Ve),new jn(Ve),new jn(Ve);let Je=Le.filter((Ge)=>Ge.includes(De));t(Je.length,1,"the duplicate-prefix warning must fire at most once per fingerprint")}}]}}async function qe(){let Be=performance.now(),{tests:Ne}=oe(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Tenant lifecycle",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}import{assertEcid as Ed,EcidFormatError as qr,formatEcid as Ad,ION_SCIM_USER_EXTENSION_URN as Id,isEcid as Pd,parseEcid as _d}from"@ionite/core";import{createControlValidators as ym,createValidators as wm,BaseUser as vm}from"@ionite/valibot";import{createControlValidators as bm,createValidators as km,BaseUser as Sm}from"@ionite/zod";var $d=km(),Md=wm(),Tm=bm(),Cm=ym();async function jd(Be,Ne){return!(await Be["~standard"].validate(Ne)).issues}async function Es(Be,Ne,Le,ze,De){let Ve=await jd(Be,Le),Je=await jd(Ne,Le);e(Ve===Je,`${De}: zod and valibot disagree (zod ${Ve?"accepted":"rejected"}, valibot ${Je?"accepted":"rejected"})`),e(Ve===ze,`${De}: expected both packages to ${ze?"accept":"reject"}`)}function Ao(){return{z:$d.sso.idTokenClaims,v:Md.sso.idTokenClaims}}function Io(){return{z:$d.iam.user,v:Md.iam.user}}function Ud(){return{z:Sm,v:vm}}var $r="ecid:acme.com:01J9F3Z8QK7M2N4P6R8T0V2W4X",Mr=["not-an-ecid","ecid:acme.com","ecid::opaque","ecid:acme.com:","urn:x:acme:1","ecid:acme com:abc"],Od={userName:"u@example.com",name:"U",email:"u@example.com"},qd={userName:"u@example.com"};function Nr(){return{z:Tm.sso.registerSSOAppPayload,v:Cm.sso.registerSSOAppPayload}}var Po={sub:"user-1",iss:"https://idp.example/realm",aud:"client-1",exp:2000000000,iat:1};function re(){return{tests:[{name:"§10.1 ID-token claims: complete claims accepted (both packages)",fn:async()=>{let{z:Ne,v:Le}=Ao();await Es(Ne,Le,Po,!0,"valid id-token claims")}},{name:"§10.1 ID-token claims: array aud accepted (both packages)",fn:async()=>{let{z:Ne,v:Le}=Ao();await Es(Ne,Le,{...Po,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:Ne,v:Le}=Ao();for(let ze of["sub","iss","aud","exp"]){let De={...Po};delete De[ze],await Es(Ne,Le,De,!1,`id-token missing ${ze}`)}}},{name:"§10.1 ID-token claims: empty aud array rejected (both packages)",fn:async()=>{let{z:Ne,v:Le}=Ao();await Es(Ne,Le,{...Po,aud:[]},!1,"empty aud array")}},{name:"§10.2 SCIM user: a present password is rejected (both packages)",fn:async()=>{let{z:Ne,v:Le}=Io();await Es(Ne,Le,{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:Ne,v:Le}=Io();await Es(Ne,Le,{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:Ne,v:Le}=Nr(),ze=["javascript:alert(1)","data:text/html,<script>alert(1)</script>","not-a-url","ftp://example.com/cb","http://evil.example.com/cb",`java
|
|
6
|
+
script:alert(1)`];for(let De of ze)await Es(Ne,Le,{redirectUris:[De]},!1,`redirectUri ${JSON.stringify(De)}`)}},{name:"§10.2 registerSSOApp: https and http://localhost redirectUris accepted (both packages)",fn:async()=>{let{z:Ne,v:Le}=Nr(),ze=["https://app.example.com/auth/callback","http://localhost:3000/cb","http://127.0.0.1:3000/cb"];for(let De of ze)await Es(Ne,Le,{redirectUris:[De]},!0,`redirectUri ${JSON.stringify(De)}`)}},{name:"§10.2 registerSSOApp: empty redirectUris array rejected (both packages)",fn:async()=>{let{z:Ne,v:Le}=Nr();await Es(Ne,Le,{redirectUris:[]},!1,"empty redirectUris")}},{name:"ECID: formatEcid/parseEcid/isEcid round-trip a valid ECID",fn:async()=>{let Ne=Ad("acme.com","01J9F3Z8QK7M2N4P6R8T0V2W4X");e(Ne===$r,`formatEcid produced ${Ne}`),e(Pd(Ne),"isEcid should accept a formatted ECID");let Le=_d(Ne);e(Le.realm==="acme.com"&&Le.opaque==="01J9F3Z8QK7M2N4P6R8T0V2W4X","parseEcid segments"),e(Ed(Ne)===Ne,"assertEcid returns the value")}},{name:"ECID: malformed values are rejected by isEcid and throw from parse/assert/format",fn:async()=>{for(let Le of Mr){e(!Pd(Le),`isEcid should reject ${JSON.stringify(Le)}`);let ze=!1;try{_d(Le)}catch(Ve){ze=Ve instanceof qr}e(ze,`parseEcid should throw EcidFormatError for ${JSON.stringify(Le)}`);let De=!1;try{Ed(Le)}catch(Ve){De=Ve instanceof qr}e(De,`assertEcid should throw EcidFormatError for ${JSON.stringify(Le)}`)}let Ne=!1;try{Ad("acme com","abc")}catch(Le){Ne=Le instanceof qr}e(Ne,"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:Ne,v:Le}=Io(),ze={...qd,[Id]:{ecid:$r,accountClass:"privileged"}};await Es(Ne,Le,ze,!0,"scim user with valid ionite extension")}},{name:"ECID: SCIM user with a malformed ecid rejected (both packages)",fn:async()=>{let{z:Ne,v:Le}=Io();for(let ze of Mr){let De={...qd,[Id]:{ecid:ze}};await Es(Ne,Le,De,!1,`scim user with malformed ecid ${JSON.stringify(ze)}`)}}},{name:"ECID: BaseUser with a valid ecid accepted (both packages)",fn:async()=>{let{z:Ne,v:Le}=Ud();await Es(Ne,Le,{...Od,ecid:$r,accountClass:"daily"},!0,"base user valid ecid")}},{name:"ECID: BaseUser with a malformed ecid rejected (both packages)",fn:async()=>{let{z:Ne,v:Le}=Ud();for(let ze of Mr)await Es(Ne,Le,{...Od,ecid:ze},!1,`base user malformed ecid ${JSON.stringify(ze)}`)}}]}}async function $e(){let Be=performance.now(),{tests:Ne}=re(),Le=[];for(let Je of Ne)Le.push(await s(Je.name,async()=>{await Je.fn();return}));let ze=performance.now()-Be,De=Le.filter((Je)=>!Je.passed).length,Ve=Le.filter((Je)=>Je.details?.skipped).length;return{suite:"Validator parity",passed:De===0,tests:Le,duration:ze,summary:{total:Le.length,passed:Le.length-De-Ve,failed:De,skipped:Ve}}}function X(Be){let{describe:Ne,it:Le,beforeAll:ze}=Be.hooks,De,Ve,Je,Ge,We=new Set,Ke=new Set,Fe={baseUrl:()=>{if(!De)throw Error("ECV setup has not completed");return De.baseUrl},ecv:()=>{if(!De)throw Error("ECV setup has not completed");return De.ecv},basePath:()=>{let Ze=Be.basePath??De?.basePath;if(!Ze)throw Error("ECV basePath must be provided in ecvSuite options or returned by setup()");return Ze},capabilitiezPath:Be.capabilitiezPath,timeout:Be.timeout??15000,skipTlsVerify:Be.skipTlsVerify,headers:Be.headers,ion:Be.ion};function He(){return Ge??=(async()=>{De=await Be.setup();try{Ve=await J(Fe)}catch(Ze){Je=Ze}})(),Ge}function Xe(Ze,tt=!0){let Qe=new Map;for(let st of Ze){let et=Qe.get(st.suite)??[];et.push(st),Qe.set(st.suite,et)}for(let[st,et]of Qe)Ne(st,()=>{ze(He);for(let nt of et)Le(nt.title,async(dt)=>{if(tt&&!Ve){let pt=Je instanceof Error?Je.message:String(Je);throw Error(`capabilitiez manifest unavailable: ${pt}`)}await nt.run(Ve,dt)})})}function Ye(Ze,tt={}){if(Ke.has(Ze))throw Error(`ECV unit ${Ze} was registered more than once`);Ke.add(Ze),We.add(Ze),Xe(F(Fe,Ze,tt))}return Xe(G(()=>[...We])),{sso:{login:(Ze)=>Ye("sso.login",Ze),jit:()=>Ye("sso.jit"),logout:()=>Ye("sso.logout"),back_channel_logout:()=>Ye("sso.back_channel_logout"),claims_authz:(Ze)=>Ye("sso.claims_authz",{ssoClaimsAuthz:Ze})},ciam:{customer:()=>Ye("ciam.customer"),magic_link:()=>Ye("ciam.magic_link"),oidc_login:()=>Ye("ciam.oidc_login"),logout:()=>Ye("ciam.logout"),claims_authz:(Ze)=>Ye("ciam.claims_authz",{ciamClaimsAuthz:Ze})},iam:{users:()=>Ye("iam.users"),groups:()=>Ye("iam.groups"),groupsCreate:()=>Ye("iam.groups.create"),workloads:()=>Ye("iam.workloads"),customers:()=>Ye("iam.customers")},workload:{incoming:()=>Ye("workload.incoming"),outgoing:()=>Ye("workload.outgoing"),claims_authz:()=>Ye("workload.claims_authz")},authz:{oaa:()=>Ye("authz.oaa"),access_requests:(Ze)=>Ye("authz.access_requests",{accessRequests:Ze})},tenants:(Ze)=>{if(Ke.has("tenant.management"))throw Error("ECV tenant management tests were registered more than once");Ke.add("tenant.management"),Xe(K(Fe,Ze),!1)},sdk:()=>{if(Ke.has("sdk"))throw Error("ECV SDK tests were registered more than once");Ke.add("sdk"),Xe(W(),!1)}}}function Me(Be){console.log(`
|
|
7
|
+
=`.repeat(60)),console.log("Enterprise Control Validation Report"),console.log("=".repeat(60)),console.log(`Timestamp: ${Be.timestamp.toISOString()}`),console.log(`Base URL: ${Be.baseUrl}`),console.log(`Duration: ${Be.duration.toFixed(2)}ms`),console.log(`Overall: ${Be.passed?"PASSED":"FAILED"}`);for(let Ne of Be.suites){console.log(`
|
|
8
|
+
${Ne.suite} Suite ${Ne.passed?"PASSED":"FAILED"}`),console.log(` Passed: ${Ne.summary.passed}/${Ne.summary.total}`+(Ne.summary.skipped>0?` (${Ne.summary.skipped} skipped)`:""));for(let Le of Ne.tests)if(console.log(` ${Le.details?.skipped?"SKIP":Le.passed?"PASS":"FAIL"} ${Le.name}`),Le.error)console.log(` Error: ${Le.error}`)}}
|
|
9
|
+
export{ie,ce,n,a,s,c,e,t,r,u,p,Z,de,m,le,ue,h,U,q,l,o,g,pe,M,N,L,z,me,f,y,he,w,ge,v,fe,i,D,Q,ee,b,ye,k,we,S,ve,T,be,ke,C,V,Se,R,Te,x,Ce,E,Re,A,xe,d,Ee,te,B,I,Ae,P,Ie,_,Pe,_e,j,J,G,W,K,F,se,H,Y,O,je,ae,Oe,ne,Ue,oe,qe,re,$e,X,Me};
|