@ionite/devtools 0.0.24-beta.20260928.18 → 0.0.24-beta.20260930.19

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 CHANGED
@@ -35,6 +35,139 @@ declare function waitForReady(baseUrl: string, options?: WaitForReadyOptions): P
35
35
  * Spawn an App in its own POSIX process group so stop() also cleans up child workers.
36
36
  */
37
37
  declare function startAppUnderTest(options: StartAppOptions): Promise<AppUnderTest>;
38
+ type DatasetLicence = {
39
+ name: string;
40
+ attribution?: string;
41
+ sourceUrl?: string;
42
+ };
43
+ type DatasetAvatar = {
44
+ url: string;
45
+ attribution?: string;
46
+ licence?: string;
47
+ sourceUrl?: string;
48
+ };
49
+ type DatasetOrgUnit = {
50
+ key: string;
51
+ name: string;
52
+ parent?: string;
53
+ leader?: string;
54
+ };
55
+ type DatasetWorkforcePerson = {
56
+ key: string;
57
+ employeeNumber: string;
58
+ email: string;
59
+ givenName: string;
60
+ middleName?: string;
61
+ familyName: string;
62
+ title?: string;
63
+ orgUnit: string;
64
+ manager?: string;
65
+ active?: boolean;
66
+ avatar?: DatasetAvatar;
67
+ };
68
+ type DatasetCustomer = {
69
+ key: string;
70
+ email: string;
71
+ givenName: string;
72
+ familyName: string;
73
+ groups?: string[];
74
+ };
75
+ type DatasetMemberSet = {
76
+ people?: string[];
77
+ orgUnits?: string[];
78
+ reportsTo?: string[];
79
+ };
80
+ type DatasetWorkflow = "auto" | "approve" | "approve-with-justification";
81
+ type DatasetSubjectKind = "user" | "customer";
82
+ type DatasetEntitlementPolicy = {
83
+ workflow?: DatasetWorkflow;
84
+ eligible?: DatasetMemberSet;
85
+ approvers?: DatasetMemberSet;
86
+ };
87
+ type DatasetAssignment = {
88
+ entitlement: string;
89
+ subjectKind: DatasetSubjectKind;
90
+ members: DatasetMemberSet;
91
+ };
92
+ type DatasetAppSection = {
93
+ policies?: Record<string, DatasetEntitlementPolicy>;
94
+ assignments?: DatasetAssignment[];
95
+ claims?: Record<string, {
96
+ groups: string[];
97
+ }>;
98
+ };
99
+ type DatasetV1 = {
100
+ version: 1;
101
+ meta: {
102
+ id: string;
103
+ version: string;
104
+ name?: string;
105
+ contentHash?: string;
106
+ licence?: DatasetLicence;
107
+ };
108
+ emailDomain: string;
109
+ orgUnits: DatasetOrgUnit[];
110
+ workforce: DatasetWorkforcePerson[];
111
+ customers: DatasetCustomer[];
112
+ apps: Record<string, DatasetAppSection>;
113
+ };
114
+ type DatasetOverlayV1 = {
115
+ apps: Record<string, DatasetAppSection>;
116
+ };
117
+ type DatasetLock = {
118
+ id: string;
119
+ version: string;
120
+ hash: string;
121
+ source: string;
122
+ };
123
+ type ResolvedDatasetMemberSet = {
124
+ workforce: ReadonlySet<string>;
125
+ customers: ReadonlySet<string>;
126
+ };
127
+ declare function validateDataset(value: unknown): DatasetV1;
128
+ declare function parseDatasetJsonc(text: string): DatasetV1;
129
+ declare function parseDatasetOverlayJsonc(text: string): DatasetOverlayV1;
130
+ declare function mergeDatasetOverlay(base: DatasetV1, overlay: DatasetOverlayV1): DatasetV1;
131
+ declare function resolveDatasetMemberSet(dataset: DatasetV1, set: DatasetMemberSet): ResolvedDatasetMemberSet;
132
+ declare function datasetContentHash(dataset: DatasetV1): string;
133
+ declare function datasetLock(dataset: DatasetV1, source: string): DatasetLock;
134
+ declare function readDatasetLock(path: string): Promise<DatasetLock>;
135
+ declare function writeDatasetLock(path: string, lock: DatasetLock): Promise<void>;
136
+ type GovernanceWorkflow = DatasetWorkflow;
137
+ declare const DEFAULT_WORKFLOWS: Readonly<Record<string, GovernanceWorkflow>>;
138
+ type EligibilityResult = {
139
+ eligible: true;
140
+ } | {
141
+ eligible: false;
142
+ reason: "not_in_eligible_set";
143
+ };
144
+ type AssignmentBinding = {
145
+ entitlementId: string;
146
+ subjectKind: "user" | "customer";
147
+ members: string[];
148
+ unresolved: string[];
149
+ };
150
+ type AssignmentPlan = {
151
+ appSlug: string;
152
+ bindings: AssignmentBinding[];
153
+ };
154
+ type AssignmentResolvers = {
155
+ user(key: string): string | undefined;
156
+ customer(key: string): string | undefined;
157
+ };
158
+ declare function findAppPolicy(dataset: DatasetV1, appSlug: string, entitlementId: string): DatasetEntitlementPolicy | undefined;
159
+ declare function resolveWorkflow(input: {
160
+ riskLevel?: string | null;
161
+ policy?: DatasetEntitlementPolicy;
162
+ overrides?: ReadonlyMap<string, GovernanceWorkflow>;
163
+ defaults?: Readonly<Record<string, GovernanceWorkflow>>;
164
+ }): GovernanceWorkflow;
165
+ declare function evaluateEligibility(dataset: DatasetV1, policy: DatasetEntitlementPolicy | undefined, subject: {
166
+ kind: "user" | "customer";
167
+ key: string;
168
+ }): EligibilityResult;
169
+ declare function resolveApprovers(dataset: DatasetV1, policy: DatasetEntitlementPolicy | undefined): string[];
170
+ declare function planAssignments(dataset: DatasetV1, appSlug: string, resolvers: AssignmentResolvers): AssignmentPlan;
38
171
  type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
39
172
  type RequestJsonOptions = {
40
173
  fetch?: FetchLike;
@@ -79,6 +212,64 @@ declare function stageTmIntegration(options: StageTmIntegrationOptions): Promise
79
212
  accessToken: string;
80
213
  }>;
81
214
  declare function tmIntegratorDriver(options: TmIntegratorDriverOptions): TmIntegratorDriver;
215
+ type DatasetControlUrls = {
216
+ hr: string;
217
+ sso: string;
218
+ iam: string;
219
+ ciam?: string;
220
+ };
221
+ type DatasetControlAuthorization = {
222
+ sso: string | (() => Promise<string>);
223
+ iam: string | (() => Promise<string>);
224
+ ciam?: string | (() => Promise<string>);
225
+ };
226
+ type DatasetLoadStageName = "hr" | "iam-sync" | "sso" | "ciam" | "iam-dataset";
227
+ type DatasetLoadStageReport = {
228
+ tenant: string;
229
+ control: DatasetLoadStageName;
230
+ created: number;
231
+ updated: number;
232
+ unchanged: number;
233
+ skipped?: "control_not_configured" | "control_lacks_dataset_api";
234
+ errors: string[];
235
+ };
236
+ type DatasetLoadReport = {
237
+ dataset: {
238
+ id: string;
239
+ version: string;
240
+ hash: string;
241
+ };
242
+ dryRun: boolean;
243
+ ok: boolean;
244
+ stages: DatasetLoadStageReport[];
245
+ };
246
+ type LoadDatasetOptions = {
247
+ dataset: DatasetV1;
248
+ overlays?: readonly DatasetOverlayV1[];
249
+ tenants: readonly string[];
250
+ controls: DatasetControlUrls;
251
+ authorization: DatasetControlAuthorization;
252
+ fetch?: FetchLike;
253
+ timeoutMs?: number;
254
+ dryRun?: boolean;
255
+ };
256
+ type DatasetHrPayload = {
257
+ orgUnits: DatasetV1["orgUnits"];
258
+ workforce: DatasetWorkforcePerson[];
259
+ dryRun?: boolean;
260
+ };
261
+ type DatasetCiamPayload = {
262
+ customers: DatasetCustomer[];
263
+ dryRun?: boolean;
264
+ };
265
+ type DatasetIdpPayload = {
266
+ dataset: Pick<DatasetV1, "orgUnits" | "workforce" | "customers">;
267
+ apps: Record<string, DatasetAppSection>;
268
+ dryRun?: boolean;
269
+ };
270
+ type DatasetSsoPayload = DatasetIdpPayload;
271
+ type DatasetIamPayload = DatasetIdpPayload;
272
+ declare function loadDataset(options: LoadDatasetOptions): Promise<DatasetLoadReport>;
82
273
  type PathFetch = (path: string, init?: RequestInit) => Promise<Response>;
83
274
  type OidcLoginConfig = {
84
275
  loginPath?: string;
@@ -90,6 +281,7 @@ declare function runLoginToCallback(config: OidcLoginConfig, fetch: PathFetch, o
90
281
  redirectParam?: string;
91
282
  bogusCode?: boolean;
92
283
  externalFetch?: FetchLike;
284
+ groups?: string[];
93
285
  }): Promise<{
94
286
  callback: Response;
95
287
  state: string | null;
@@ -133,6 +325,18 @@ type ProvisionedTenant = {
133
325
  * Provision a fixture tenant through an App's real Tenant Management contract.
134
326
  */
135
327
  declare function provisionTenant(options: ProvisionTenantOptions): Promise<ProvisionedTenant>;
328
+ import { GroupResource } from "@ionite/core";
329
+ type ScimGroupsDiscovery = {
330
+ supported: false;
331
+ } | {
332
+ supported: true;
333
+ groupsUrl: string;
334
+ };
335
+ declare function discoverScimGroups(scimBaseUrl: string, token: string): Promise<ScimGroupsDiscovery>;
336
+ declare function fetchScimGroupsFromUrl(groupsUrl: string, token: string, options?: {
337
+ requestableOnly?: boolean;
338
+ }): Promise<GroupResource[]>;
339
+ declare function getScimGroupById(scimBaseUrl: string, token: string, groupId: string): Promise<GroupResource | null>;
136
340
  /**
137
341
  * Small origin-scoped cookie session for HTTP-only development drivers.
138
342
  *
@@ -206,7 +410,7 @@ type DevIntegrationIntent = {
206
410
  groups?: boolean;
207
411
  delta?: "patch";
208
412
  customers?: {
209
- assignment?: "app" | "iam" | "iam-request";
413
+ assignment?: "app" | "iam" | "iam-request" | "idp";
210
414
  };
211
415
  };
212
416
  oaa?: {
@@ -264,63 +468,4 @@ type SeedIoniteTenantOptions = {
264
468
  * caller starts the App, and only then may ECI invoke the App's Tenant Management endpoint.
265
469
  */
266
470
  declare function seedIoniteTenant(options: SeedIoniteTenantOptions): Promise<SeededIoniteTenant>;
267
- type WorkforceUserFixture = {
268
- key: string;
269
- externalId?: string;
270
- userName: string;
271
- displayName?: string;
272
- active?: boolean;
273
- emails?: Array<{
274
- value: string;
275
- primary?: boolean;
276
- type?: string;
277
- }>;
278
- name?: {
279
- givenName?: string;
280
- familyName?: string;
281
- formatted?: string;
282
- };
283
- attributes?: Record<string, unknown>;
284
- };
285
- type WorkforceGroupFixture = {
286
- key: string;
287
- externalId?: string;
288
- displayName: string;
289
- attributes?: Record<string, unknown>;
290
- };
291
- type WorkforceMembershipFixture = {
292
- group: string;
293
- users: string[];
294
- };
295
- type WorkforceFixtureV1 = {
296
- version: 1;
297
- users: WorkforceUserFixture[];
298
- groups: WorkforceGroupFixture[];
299
- memberships?: WorkforceMembershipFixture[];
300
- };
301
- type SeedWorkforceOptions = {
302
- appBaseUrl: string;
303
- scimPath?: string;
304
- fixture: WorkforceFixtureV1;
305
- authorization: string | (() => Promise<string>);
306
- /**
307
- * Materialize App-owned employee records before SCIM adds IAM metadata. Required when the App,
308
- * like LEO, is the workforce system of record and deliberately rejects SCIM-created users.
309
- */
310
- prepareUsers?: (users: readonly WorkforceUserFixture[]) => Promise<void>;
311
- fetch?: FetchLike;
312
- timeoutMs?: number;
313
- };
314
- type SeededWorkforce = {
315
- users: ReadonlyMap<string, string>;
316
- groups: ReadonlyMap<string, string>;
317
- };
318
- /**
319
- * Replay a versioned workforce fixture against an App's SCIM surface.
320
- *
321
- * Stages preserve referential integrity while parallelizing independent writes: users, groups, then
322
- * membership replacement. Re-running the same fixture updates rows by externalId instead of
323
- * duplicating them.
324
- */
325
- declare function seedWorkforce(options: SeedWorkforceOptions): Promise<SeededWorkforce>;
326
- export { AppUnderTest, DevControlModule, DevIntegrationIntent, DevProductRegistration, FetchLike, HttpSession, OidcLoginConfig, PathFetch, ProvisionTenantOptions, ProvisionedTenant, RegisteredDevProduct, RequestJsonOptions, SeedAppOwnerOptions, SeedIoniteTenantOptions, SeedWorkforceOptions, SeededAppOwner, SeededIoniteTenant, SeededWorkforce, StageTmIntegrationOptions, StartAppOptions, TenantManagementControl, TenantProvisioningProfile, TmIntegratorDriver, TmIntegratorDriverOptions, TmRedeemResult, WaitForReadyOptions, WorkforceFixtureV1, WorkforceGroupFixture, WorkforceMembershipFixture, WorkforceUserFixture, completeLoginFlow, provisionTenant, requestJson, resolveReadyPath, runLoginToCallback, seedAppOwner, seedIoniteTenant, seedWorkforce, stageTmIntegration, startAppUnderTest, tmIntegratorDriver, waitForReady };
471
+ export { AppUnderTest, AssignmentBinding, AssignmentPlan, AssignmentResolvers, DEFAULT_WORKFLOWS, DatasetAppSection, DatasetAssignment, DatasetAvatar, DatasetCiamPayload, DatasetControlAuthorization, DatasetControlUrls, DatasetCustomer, DatasetEntitlementPolicy, DatasetHrPayload, DatasetIamPayload, DatasetIdpPayload, DatasetLicence, DatasetLoadReport, DatasetLoadStageName, DatasetLoadStageReport, DatasetLock, DatasetMemberSet, DatasetOrgUnit, DatasetOverlayV1, DatasetSsoPayload, DatasetSubjectKind, DatasetV1, DatasetWorkflow, DatasetWorkforcePerson, DevControlModule, DevIntegrationIntent, DevProductRegistration, EligibilityResult, FetchLike, GovernanceWorkflow, HttpSession, LoadDatasetOptions, OidcLoginConfig, PathFetch, ProvisionTenantOptions, ProvisionedTenant, RegisteredDevProduct, RequestJsonOptions, ResolvedDatasetMemberSet, ScimGroupsDiscovery, SeedAppOwnerOptions, SeedIoniteTenantOptions, SeededAppOwner, SeededIoniteTenant, StageTmIntegrationOptions, StartAppOptions, TenantManagementControl, TenantProvisioningProfile, TmIntegratorDriver, TmIntegratorDriverOptions, TmRedeemResult, WaitForReadyOptions, completeLoginFlow, datasetContentHash, datasetLock, discoverScimGroups, evaluateEligibility, fetchScimGroupsFromUrl, findAppPolicy, getScimGroupById, loadDataset, mergeDatasetOverlay, parseDatasetJsonc, parseDatasetOverlayJsonc, planAssignments, provisionTenant, readDatasetLock, requestJson, resolveApprovers, resolveDatasetMemberSet, resolveReadyPath, resolveWorkflow, runLoginToCallback, seedAppOwner, seedIoniteTenant, stageTmIntegration, startAppUnderTest, tmIntegratorDriver, validateDataset, waitForReady, writeDatasetLock };
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
- import{spawn as B}from"node:child_process";import{ACCEPT_JSON as G}from"@ionite/core";import{JSON_MEDIA_TYPE as q,readJsonResponse as S,unexpectedBodySummary as O}from"@ionite/core";function l(e){return e.replace(/\/+$/,"")}async function m(e,r){if(!e.ok){let t=await O(e);throw Error(`${r} failed (${e.status})${t.excerpt?`: ${t.excerpt}`:""}`)}return await S(e)}async function h(e,r,t={}){let n=new Headers(r.headers);if(!n.has("Accept"))n.set("Accept",q);let o=await(t.fetch??globalThis.fetch)(e,{...r,headers:n,signal:r.signal??AbortSignal.timeout(t.timeoutMs??30000)});if(!o.ok){let i=await O(o);throw Error(`${t.label??`${r.method??"GET"} ${e}`} failed (${o.status})${i.excerpt?`: ${i.excerpt}`:""}`)}if(o.status===204)return;return await S(o)}var H="/api/ionite",K=5000,V=2000;function Y(e){return new Promise((r)=>setTimeout(r,e))}function I(e,r){if(e.exitCode!==null||e.signalCode!==null)return Promise.resolve(!0);return new Promise((t)=>{let n=!1,o=()=>{if(n)return;n=!0,clearTimeout(i),t(!0)},i=setTimeout(()=>{if(n)return;n=!0,e.removeListener("exit",o),t(!1)},r);e.once("exit",o)})}function L(e){if(e.readyPath)return e.readyPath;return`${l(e.basePath??H)}/readyz`}async function y(e,r={}){let{livenessPath:t,maxRetries:n=30,retryDelayMs:o=1000,requestTimeoutMs:i=3000}=r,s=L(r),d;for(let p=0;p<n;p++){try{if(t){let g=await fetch(new URL(t,e),{signal:AbortSignal.timeout(i)});if(!g.ok)throw Error(`liveness returned ${g.status}`)}let a=await fetch(new URL(s,e),{headers:G,signal:AbortSignal.timeout(i)});if(a.ok)return;d=Error(`readiness returned ${a.status}`)}catch(a){d=a}if(p<n-1)await Y(o)}let c=d instanceof Error?d.message:String(d);throw Error(`App not ready at ${new URL(s,e)} after ${n} attempts: ${c}`)}async function j(e){let{command:r,args:t=[],cwd:n,env:o,baseUrl:i,...s}=e,d=process.platform!=="win32",c=B(r,t,{cwd:n,env:{...process.env,...o},stdio:"inherit",detached:d});c.on("error",(u)=>{console.error(`Failed to start app under test (${r}):`,u)});let p=(u)=>{try{if(d&&c.pid!==void 0)process.kill(-c.pid,u);else c.kill(u)}catch{}},a=()=>p("SIGKILL");process.once("exit",a);let g=async()=>{if(c.exitCode===null&&c.signalCode===null){if(p("SIGTERM"),!await I(c,K))p("SIGKILL"),await I(c,V)}p("SIGKILL"),process.removeListener("exit",a)};try{await y(i,s)}catch(u){throw await g(),u}return{baseUrl:i,...c.pid===void 0?{}:{pid:c.pid},stop:g}}import{ACCEPT_JSON as v,CONTENT_TYPE_JSON as E}from"@ionite/core";async function Q(e){let r=new URL(`${l(e.integratorBaseUrl)}/api/tm/stage`),t=await fetch(r,{method:"POST",headers:{...v,...E},body:JSON.stringify({audience:e.audience,integratorTenantId:e.integratorTenantId,productId:e.productId,applicationId:e.applicationId,tenantHint:e.tenantHint,ttlSeconds:e.ttlSeconds})});return m(t,"TM stage")}function X(e){let r=new URL(l(e.tenantPath??"/api/tenants"),e.appBaseUrl),t=()=>AbortSignal.timeout(e.timeoutMs??15000),n=(i)=>new URL(`${r.pathname}/integrations/${i}`,r),o=(i,s)=>fetch(n(i),{method:"POST",headers:{...v,Authorization:`Bearer ${e.accessToken}`,...E},body:JSON.stringify(s),signal:t()});return{authorizeUrl(){let i=n("authorize");return i.searchParams.set("handle",e.handle),i.toString()},async redeem(){let i=await o("redeem",{handle:e.handle});if(![200,202,410,429].includes(i.status))throw Error(`TM redeem failed (${i.status}): ${await i.text()}`);return m(i,"TM redeem")},async exportConfig(i){let s=await o("config-export",{handle:e.handle,authorization:i});return m(s,"TM config export")}}}function P(e){if(typeof e.getSetCookie==="function")return e.getSetCookie();let r=e.get("set-cookie");return r?[r]:[]}function b(e){let r=e.split(";",1)[0],t=r?.indexOf("=")??-1;if(!r||t<=0)return;return[r.slice(0,t).trim(),r.slice(t+1).trim()]}class T{#e;#t;constructor(e=globalThis.fetch){this.#e=new Map;this.#t=e}cookies(e){if(e)return new Map(this.#e.get(new URL(e).origin));let r=new Map;for(let t of this.#e.values())for(let[n,o]of t)r.set(n,o);return r}async fetch(e,r={}){let t=new URL(e),n=r.method??"GET",o=r.body;for(let i=0;i<=10;i++){let s=new Headers(r.headers),d=this.#e.get(t.origin);if(d?.size&&!s.has("Cookie"))s.set("Cookie",[...d].map(([a,g])=>`${a}=${g}`).join("; "));let c=await this.#t(t,{...r,method:n,body:o,headers:s,redirect:"manual"});if(this.#r(t.origin,c.headers),![301,302,303,307,308].includes(c.status))return c;let p=c.headers.get("location");if(!p)return c;if(t=new URL(p,t),c.status===303||(c.status===301||c.status===302)&&n==="POST")n="GET",o=void 0}throw Error(`Too many redirects while requesting ${e}`)}#r(e,r){let t=this.#e.get(e)??new Map;for(let n of P(r)){let o=b(n);if(!o)continue;let[i,s]=o;if(s===""||/(?:^|;)\s*Max-Age=0(?:;|$)/i.test(n)||/expires=Thu, 01 Jan 1970/i.test(n))t.delete(i);else t.set(i,s)}this.#e.set(e,t)}}function M(e){let r=new Map;for(let t of P(e)){let n=b(t);if(n)r.set(n[0],n[1])}return r}function C(e){return[...e].map(([r,t])=>`${r}=${t}`).join("; ")}async function A(e,r,t={}){if(!e.loginPath)throw Error("Login path not configured");let n=t.redirectParam?`?redirect=${encodeURIComponent(t.redirectParam)}`:"",o=await r(`${e.loginPath}${n}`);if(o.status!==302)throw Error(`Login did not redirect: ${o.status}`);let i=o.headers.get("location");if(!i)throw Error("No redirect location from login");let s=M(o.headers),d=new URL(i).searchParams.get("state");if(t.bogusCode){let g=new URL(i).searchParams.get("redirect_uri");if(!g||!d)throw Error("Missing redirect_uri/state on authorization URL");let u=new URL(g).pathname;return{callback:await r(`${u}?code=bogus-code&state=${encodeURIComponent(d)}`,{headers:{Cookie:C(s)}}),state:d}}let c=await(t.externalFetch??globalThis.fetch)(i,{redirect:"manual"});if(c.status!==302)throw Error(`Auth did not redirect: ${c.status}`);let p=c.headers.get("location");if(!p)throw Error("No redirect location from auth");return{callback:await r(`${new URL(p).pathname}${new URL(p).search}`,{headers:{Cookie:C(s)}}),state:d}}async function Z(e,r){try{let{callback:t}=await A(e,r);if(t.status!==302)return{cookies:new Map,success:!1,error:`Callback did not redirect: ${t.status}`};return{cookies:M(t.headers),success:!0}}catch(t){return{cookies:new Map,success:!1,error:t instanceof Error?t.message:String(t)}}}import{ACCEPT_JSON as F,CONTENT_TYPE_JSON as ee,readJsonResponse as N}from"@ionite/core";function f(e,r){if(!e)throw Error(r)}async function te(e){let r=l(e.tenantPath??"/api/tenants"),t=e.profile??"tenantManager",n=await e.control.getTenantManagementToken(),o={...e.tenant,configLocator:e.tenant.configLocator??e.control.configLocator,webhookUrl:e.tenant.webhookUrl??e.control.webhookUrl},i=await fetch(new URL(r,e.baseUrl),{method:"POST",headers:{...F,Authorization:`Bearer ${n}`,...ee},body:JSON.stringify(o),signal:AbortSignal.timeout(e.readiness?.requestTimeoutMs??15000)}),s=await N(i).catch(()=>({}));if(i.status===409||s.status==="failed")throw Error(`Tenant ${o.id} provisioning failed: ${s.error??i.statusText}`);if(s.status==="action_required")throw Error(`Tenant ${o.id} provisioning requires action at ${s.actionUrl}`);if(f(i.status===200||i.status===201||i.status===202,`Expected tenant create to return 200, 201, or 202, got ${i.status}`),f(typeof s.tenantUrl==="string",`Tenant ${o.id} response must include tenantUrl`),t==="creation"){let u=l(s.tenantUrl),w=e.basePath??"/api/ionite";return await y(u,{...e.readiness,basePath:w}),{id:o.id,baseUrl:u,basePath:w,tenantUrl:s.tenantUrl}}let d=await fetch(new URL(`${r}/${encodeURIComponent(o.id)}`,e.baseUrl),{headers:{...F,Authorization:`Bearer ${n}`},signal:AbortSignal.timeout(e.readiness?.requestTimeoutMs??15000)});f(d.status===200,`Expected tenant GET to return 200, got ${d.status}`);let c=await N(d);f(c.id===o.id,`Expected tenant GET id ${o.id}`),f(typeof c.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl"),f(typeof c.ionUrls.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl");let p=new URL(c.ionUrls.capabilitiezUrl);f(p.pathname.endsWith("/capabilitiez"),"Tenant capabilitiezUrl must end in /capabilitiez");let a=p.pathname.slice(0,-13)||"/",g=new URL(c.ionUrls.readyzUrl);return await y(p.origin,{...e.readiness,readyPath:`${g.pathname}${g.search}`}),{id:o.id,baseUrl:p.origin,basePath:a,tenantUrl:typeof c.tenantUrl==="string"?c.tenantUrl:s.tenantUrl}}import{ACCEPT_JSON as _,CONTENT_TYPE_JSON as z}from"@ionite/core";async function re(e){let r=l(e.appBaseUrl),t=new T(e.fetch),n=()=>AbortSignal.timeout(e.timeoutMs??30000),o=await t.fetch(new URL(e.magicLinkPath??"/api/onboard/magic-link",r),{method:"POST",headers:{..._,...z},body:JSON.stringify(e.magicLinkBody?.(e.owner)??{email:e.owner.email}),signal:n()}),i=await m(o,"App owner magic-link creation"),s=e.readMagicLink?.(i)??(typeof i==="object"&&i!==null&&typeof i.magicLink==="string"?i.magicLink:void 0);if(!s)throw Error("App owner magic-link response did not include magicLink");let d=await t.fetch(new URL(s,r),{signal:n()});if(!d.ok)throw Error(`App owner magic-link login failed (${d.status}): ${await d.text()}`);let c=await t.fetch(new URL(e.tenantCreatePath??"/api/onboard",r),{method:"POST",headers:{..._,...z},body:JSON.stringify(e.tenantBody?.(e.tenant)??{slug:e.tenant.id,companyName:e.tenant.name}),signal:n()}),p=await m(c,"App tenant creation");return{tenantId:e.tenant.id,owner:e.owner,tenant:p,session:t}}import{ACCEPT_JSON as ne,CONTENT_TYPE_JSON as k}from"@ionite/core";async function ie(e,r,t){let n=Date.now();while(Date.now()-n<t){try{if((await r(`${e}/api/readyz`,{headers:ne,signal:AbortSignal.timeout(2000)})).ok)return}catch{}await new Promise((o)=>setTimeout(o,500))}throw Error(`Timed out waiting for the ECI at ${e}/api/readyz`)}async function oe(e){let r=process.env.ION_CONTROLS_PORT?.trim()||"3700",t=l(e.eciBaseUrl??`http://localhost:${r}/ionite`),n=e.fetch??globalThis.fetch,o={fetch:n,timeoutMs:e.requestTimeoutMs??120000};if(await ie(t,n,e.readyTimeoutMs??300000),await h(`${t}/api/bootstrap`,{method:"POST",headers:k,body:JSON.stringify({companies:[e.company]})},{...o,label:"ECI company bootstrap"}),e.oem)await h(`${t}/api/oems`,{method:"POST",headers:k,body:JSON.stringify(e.oem)},{...o,label:"ECI OEM registration"});let i=await h(`${t}/api/products`,{method:"POST",headers:k,body:JSON.stringify(e.product)},{...o,label:"ECI product registration"});await e.onProductRegistered?.(i);let s=await h(`${t}/api/companies/${encodeURIComponent(e.company.id)}/products/${encodeURIComponent(i.product.id)}/integration`,{method:"POST",headers:k,body:JSON.stringify(e.integration)},{...o,label:"ECI tenant integration"});if(s.tenant.status==="error")throw Error(s.tenant.error??`Tenant ${e.company.id} failed to integrate`);return{registration:i,integration:s}}import{ACCEPT_SCIM as se,CONTENT_TYPE_SCIM as ae,readJsonResponse as J,unexpectedBodySummary as ce}from"@ionite/core";var de="urn:ietf:params:scim:schemas:core:2.0:User",D="urn:ietf:params:scim:schemas:core:2.0:Group";function pe(e){return e.replaceAll("\\","\\\\").replaceAll('"',"\\\"")}function ge(e){if(e.version!==1)throw Error(`Unsupported workforce fixture version: ${e.version}`);let r=new Set,t=new Set;for(let n of e.users){if(!n.key||!n.userName)throw Error("Every workforce user requires key and userName");if(r.has(n.key))throw Error(`Duplicate workforce user key: ${n.key}`);r.add(n.key)}for(let n of e.groups){if(!n.key||!n.displayName)throw Error("Every workforce group requires key and displayName");if(t.has(n.key))throw Error(`Duplicate workforce group key: ${n.key}`);t.add(n.key)}for(let n of e.memberships??[]){if(!t.has(n.group))throw Error(`Unknown workforce group key: ${n.group}`);for(let o of n.users)if(!r.has(o))throw Error(`Unknown workforce user key: ${o}`)}}async function W(e,r,t,n,o){let i=await e(r,{...n,headers:{Authorization:t,...se,...n.body?ae:{},...n.headers},signal:n.signal??AbortSignal.timeout(o)});if(!i.ok){let s=await ce(i);throw Error(`SCIM ${n.method??"GET"} ${r.pathname} failed (${i.status})${s.excerpt?`: ${s.excerpt}`:""}`)}return i}async function x(e,r,t,n,o,i,s){let d=new URL(`${r.pathname.replace(/\/$/,"")}/${t}`,r);d.searchParams.set("filter",`externalId eq "${pe(n)}"`);let p=(await J(await W(e,d,i,{},s))).Resources?.[0]?.id,a=typeof p==="string"?new URL(`${d.pathname}/${encodeURIComponent(p)}`,d):d,g=await W(e,a,i,{method:typeof p==="string"?"PUT":"POST",body:JSON.stringify(o)},s),u=await J(g);if(typeof u.id!=="string")throw Error(`SCIM ${t} upsert response did not include id`);return u.id}async function ue(e){ge(e.fixture);let r=e.fetch??globalThis.fetch,t=e.timeoutMs??30000,n=typeof e.authorization==="string"?e.authorization:await e.authorization(),o=n.startsWith("Bearer ")?n:`Bearer ${n}`,i=e.scimPath??"/api/ionite/iam",s=new URL(`${l(i)}/`,e.appBaseUrl),d=new Map,c=new Map;await e.prepareUsers?.(e.fixture.users),await Promise.all(e.fixture.users.map(async(a)=>{let g=a.externalId??a.key,u=await x(r,s,"Users",g,{schemas:[de],externalId:g,userName:a.userName,displayName:a.displayName,active:a.active??!0,emails:a.emails,name:a.name,...a.attributes},o,t);d.set(a.key,u)})),await Promise.all(e.fixture.groups.map(async(a)=>{let g=a.externalId??a.key,u=await x(r,s,"Groups",g,{schemas:[D],externalId:g,displayName:a.displayName,members:[],...a.attributes},o,t);c.set(a.key,u)}));let p=new Map;for(let a of e.fixture.memberships??[]){let g=p.get(a.group)??new Set;for(let u of a.users)g.add(u);p.set(a.group,g)}return await Promise.all(e.fixture.groups.map(async(a)=>{if(!c.get(a.key))throw Error(`Missing seeded group id for ${a.key}`);let u=p.get(a.key);if(!u?.size)return;let w=[...u].map((U)=>{let R=d.get(U);if(!R)throw Error(`Missing seeded user id for ${U}`);return{value:R,type:"User"}});await x(r,s,"Groups",a.externalId??a.key,{schemas:[D],externalId:a.externalId??a.key,displayName:a.displayName,members:w,...a.attributes},o,t)})),{users:d,groups:c}}export{T as HttpSession,Z as completeLoginFlow,te as provisionTenant,h as requestJson,L as resolveReadyPath,A as runLoginToCallback,re as seedAppOwner,oe as seedIoniteTenant,ue as seedWorkforce,Q as stageTmIntegration,j as startAppUnderTest,X as tmIntegratorDriver,y as waitForReady};
1
+ import{spawn as pe}from"node:child_process";import{ACCEPT_JSON as fe}from"@ionite/core";import{JSON_MEDIA_TYPE as ge,readJsonResponse as B,unexpectedBodySummary as K}from"@ionite/core";function p(e){return e.replace(/\/+$/,"")}async function D(e,t){if(!e.ok){let r=await K(e);throw Error(`${t} failed (${e.status})${r.excerpt?`: ${r.excerpt}`:""}`)}return await B(e)}async function S(e,t,r={}){let n=new Headers(t.headers);if(!n.has("Accept"))n.set("Accept",ge);let o=await(r.fetch??globalThis.fetch)(e,{...t,headers:n,signal:t.signal??AbortSignal.timeout(r.timeoutMs??30000)});if(!o.ok){let s=await K(o);throw Error(`${r.label??`${t.method??"GET"} ${e}`} failed (${o.status})${s.excerpt?`: ${s.excerpt}`:""}`)}if(o.status===204)return;return await B(o)}var ye="/api/ionite",he=5000,we=2000;function ke(e){return new Promise((t)=>setTimeout(t,e))}function Y(e,t){if(e.exitCode!==null||e.signalCode!==null)return Promise.resolve(!0);return new Promise((r)=>{let n=!1,o=()=>{if(n)return;n=!0,clearTimeout(s),r(!0)},s=setTimeout(()=>{if(n)return;n=!0,e.removeListener("exit",o),r(!1)},t);e.once("exit",o)})}function Q(e){if(e.readyPath)return e.readyPath;return`${p(e.basePath??ye)}/readyz`}async function v(e,t={}){let{livenessPath:r,maxRetries:n=30,retryDelayMs:o=1000,requestTimeoutMs:s=3000}=t,a=Q(t),i;for(let u=0;u<n;u++){try{if(r){let l=await fetch(new URL(r,e),{signal:AbortSignal.timeout(s)});if(!l.ok)throw Error(`liveness returned ${l.status}`)}let d=await fetch(new URL(a,e),{headers:fe,signal:AbortSignal.timeout(s)});if(d.ok)return;i=Error(`readiness returned ${d.status}`)}catch(d){i=d}if(u<n-1)await ke(o)}let c=i instanceof Error?i.message:String(i);throw Error(`App not ready at ${new URL(a,e)} after ${n} attempts: ${c}`)}async function be(e){let{command:t,args:r=[],cwd:n,env:o,baseUrl:s,...a}=e,i=process.platform!=="win32",c=pe(t,r,{cwd:n,env:{...process.env,...o},stdio:"inherit",detached:i});c.on("error",(g)=>{console.error(`Failed to start app under test (${t}):`,g)});let u=(g)=>{try{if(i&&c.pid!==void 0)process.kill(-c.pid,g);else c.kill(g)}catch{}},d=()=>u("SIGKILL");process.once("exit",d);let l=async()=>{if(c.exitCode===null&&c.signalCode===null){if(u("SIGTERM"),!await Y(c,he))u("SIGKILL"),await Y(c,we)}u("SIGKILL"),process.removeListener("exit",d)};try{await v(s,a)}catch(g){throw await l(),g}return{baseUrl:s,...c.pid===void 0?{}:{pid:c.pid},stop:l}}import{createHash as De}from"node:crypto";import{readFile as Te,writeFile as Se}from"node:fs/promises";import{parse as X,printParseErrorCode as Z}from"jsonc-parser";function ee(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}function f(e,t){if(!ee(e))throw Error(`${t} must be an object`);return e}function R(e,t){if(!Array.isArray(e))throw Error(`${t} must be an array`);return e}function m(e,t){if(typeof e!=="string"||!e.trim())throw Error(`${t} must be a non-empty string`);return e}function y(e,t){if(e===void 0)return;return m(e,t)}function $e(e,t){if(e===void 0)return;if(typeof e!=="boolean")throw Error(`${t} must be a boolean`);return e}function U(e,t){return R(e,t).map((r,n)=>m(r,`${t}[${n}]`))}function M(e,t){let r=new Set;for(let n of e){if(r.has(n.key))throw Error(`Duplicate ${t} key: ${n.key}`);r.add(n.key)}return r}function Pe(e,t){if(e===void 0)return;let r=f(e,t);return{name:m(r.name,`${t}.name`),attribution:y(r.attribution,`${t}.attribution`),sourceUrl:y(r.sourceUrl,`${t}.sourceUrl`)}}function ve(e,t){if(e===void 0)return;let r=f(e,t);return{url:m(r.url,`${t}.url`),attribution:y(r.attribution,`${t}.attribution`),licence:y(r.licence,`${t}.licence`),sourceUrl:y(r.sourceUrl,`${t}.sourceUrl`)}}function N(e,t){let r=f(e,t),n={people:r.people===void 0?void 0:U(r.people,`${t}.people`),orgUnits:r.orgUnits===void 0?void 0:U(r.orgUnits,`${t}.orgUnits`),reportsTo:r.reportsTo===void 0?void 0:U(r.reportsTo,`${t}.reportsTo`)};if(!n.people?.length&&!n.orgUnits?.length&&!n.reportsTo?.length)throw Error(`${t} must select at least one person, org unit, or manager`);return n}function te(e,t){let r=f(e,t),n;if(r.policies!==void 0){n={};for(let[a,i]of Object.entries(f(r.policies,`${t}.policies`))){let c=f(i,`${t}.policies.${a}`),u=y(c.workflow,`${t}.policies.${a}.workflow`);if(u!==void 0&&u!=="auto"&&u!=="approve"&&u!=="approve-with-justification")throw Error(`${t}.policies.${a}.workflow is not supported`);n[a]={workflow:u,eligible:c.eligible===void 0?void 0:N(c.eligible,`${t}.policies.${a}.eligible`),approvers:c.approvers===void 0?void 0:N(c.approvers,`${t}.policies.${a}.approvers`)}}}let o=r.assignments===void 0?void 0:R(r.assignments,`${t}.assignments`).map((a,i)=>{let c=f(a,`${t}.assignments[${i}]`),u=m(c.subjectKind,`${t}.assignments[${i}].subjectKind`);if(u!=="user"&&u!=="customer")throw Error(`${t}.assignments[${i}].subjectKind is not supported`);return{entitlement:m(c.entitlement,`${t}.assignments[${i}].entitlement`),subjectKind:u,members:N(c.members,`${t}.assignments[${i}].members`)}}),s;if(r.claims!==void 0){s={};for(let[a,i]of Object.entries(f(r.claims,`${t}.claims`))){let c=f(i,`${t}.claims.${a}`);s[a]={groups:U(c.groups,`${t}.claims.${a}.groups`)}}}return{policies:n,assignments:o,claims:s}}function Re(e){let t=M(e.orgUnits,"org unit"),r=M(e.workforce,"workforce person"),n=M(e.customers,"customer"),o=new Set,s=new Set;for(let i of e.orgUnits){if(i.parent&&!t.has(i.parent))throw Error(`Unknown parent org unit: ${i.parent}`);if(i.leader&&!r.has(i.leader))throw Error(`Unknown org unit leader: ${i.leader}`)}for(let i of e.workforce){if(!t.has(i.orgUnit))throw Error(`Unknown org unit for ${i.key}: ${i.orgUnit}`);if(i.manager&&!r.has(i.manager))throw Error(`Unknown manager for ${i.key}: ${i.manager}`);if(o.has(i.employeeNumber))throw Error(`Duplicate workforce employeeNumber: ${i.employeeNumber}`);o.add(i.employeeNumber);let c=i.email.toLowerCase();if(s.has(c))throw Error(`Duplicate dataset email: ${i.email}`);s.add(c)}for(let i of e.customers){let c=i.email.toLowerCase();if(s.has(c))throw Error(`Duplicate dataset email: ${i.email}`);s.add(c)}let a=(i,c,u)=>{for(let d of i.people??[])if(!(u==="customer"?n.has(d):r.has(d)||n.has(d)))throw Error(`${c} references unknown person: ${d}`);if(u==="customer"&&((i.orgUnits?.length??0)>0||(i.reportsTo?.length??0)>0))throw Error(`${c} cannot select customer members by workforce organization`);for(let d of i.orgUnits??[])if(!t.has(d))throw Error(`${c} references unknown org unit: ${d}`);for(let d of i.reportsTo??[])if(!r.has(d))throw Error(`${c} references unknown manager: ${d}`)};for(let[i,c]of Object.entries(e.apps)){for(let[u,d]of Object.entries(c.policies??{})){if(d.eligible)a(d.eligible,`apps.${i}.policies.${u}.eligible`);if(d.approvers)a(d.approvers,`apps.${i}.policies.${u}.approvers`)}for(let[u,d]of(c.assignments??[]).entries())a(d.members,`apps.${i}.assignments[${u}].members`,d.subjectKind)}}function P(e){let t=f(e,"dataset");if(t.version!==1)throw Error(`Unsupported dataset version: ${String(t.version)}`);let r=f(t.meta,"dataset.meta"),n={version:1,meta:{id:m(r.id,"dataset.meta.id"),version:m(r.version,"dataset.meta.version"),name:y(r.name,"dataset.meta.name"),contentHash:y(r.contentHash,"dataset.meta.contentHash"),licence:Pe(r.licence,"dataset.meta.licence")},emailDomain:m(t.emailDomain,"dataset.emailDomain"),orgUnits:R(t.orgUnits,"dataset.orgUnits").map((o,s)=>{let a=f(o,`dataset.orgUnits[${s}]`);return{key:m(a.key,`dataset.orgUnits[${s}].key`),name:m(a.name,`dataset.orgUnits[${s}].name`),parent:y(a.parent,`dataset.orgUnits[${s}].parent`),leader:y(a.leader,`dataset.orgUnits[${s}].leader`)}}),workforce:R(t.workforce,"dataset.workforce").map((o,s)=>{let a=f(o,`dataset.workforce[${s}]`);return{key:m(a.key,`dataset.workforce[${s}].key`),employeeNumber:m(a.employeeNumber,`dataset.workforce[${s}].employeeNumber`),email:m(a.email,`dataset.workforce[${s}].email`),givenName:m(a.givenName,`dataset.workforce[${s}].givenName`),middleName:y(a.middleName,`dataset.workforce[${s}].middleName`),familyName:m(a.familyName,`dataset.workforce[${s}].familyName`),title:y(a.title,`dataset.workforce[${s}].title`),orgUnit:m(a.orgUnit,`dataset.workforce[${s}].orgUnit`),manager:y(a.manager,`dataset.workforce[${s}].manager`),active:$e(a.active,`dataset.workforce[${s}].active`),avatar:ve(a.avatar,`dataset.workforce[${s}].avatar`)}}),customers:R(t.customers,"dataset.customers").map((o,s)=>{let a=f(o,`dataset.customers[${s}]`);return{key:m(a.key,`dataset.customers[${s}].key`),email:m(a.email,`dataset.customers[${s}].email`),givenName:m(a.givenName,`dataset.customers[${s}].givenName`),familyName:m(a.familyName,`dataset.customers[${s}].familyName`),groups:a.groups===void 0?void 0:U(a.groups,`dataset.customers[${s}].groups`)}}),apps:{}};for(let[o,s]of Object.entries(f(t.apps,"dataset.apps")))n.apps[o]=te(s,`dataset.apps.${o}`);return Re(n),n}function Ue(e){let t=[],r=X(e,t,{allowTrailingComma:!0,disallowComments:!1});if(t.length>0){let n=t[0];throw Error(`Dataset JSONC parse error at offset ${n.offset}: ${Z(n.error)}`)}return P(r)}function Oe(e){let t=[],r=X(e,t,{allowTrailingComma:!0,disallowComments:!1});if(t.length>0){let s=t[0];throw Error(`Dataset overlay JSONC parse error at offset ${s.offset}: ${Z(s.error)}`)}let n=f(r,"overlay"),o={};for(let[s,a]of Object.entries(f(n.apps,"overlay.apps")))o[s]=te(a,`overlay.apps.${s}`);return{apps:o}}function j(e,t){let r=structuredClone(e.apps);for(let[n,o]of Object.entries(t.apps)){let s=r[n]??{};r[n]={policies:o.policies?{...s.policies,...o.policies}:s.policies,assignments:o.assignments??s.assignments,claims:o.claims?{...s.claims,...o.claims}:s.claims}}return P({...structuredClone(e),apps:r})}function O(e,t){let r=new Set,n=new Set,o=new Map(e.workforce.map((l)=>[l.key,l])),s=new Set(e.customers.map((l)=>l.key)),a=new Map;for(let l of e.orgUnits){if(!l.parent)continue;let g=a.get(l.parent)??[];g.push(l.key),a.set(l.parent,g)}let i=new Set,c=[...t.orgUnits??[]];while(c.length>0){let l=c.shift();if(!l||i.has(l))continue;i.add(l),c.push(...a.get(l)??[])}for(let l of e.workforce)if(i.has(l.orgUnit))r.add(l.key);let u=new Map;for(let l of e.workforce){if(!l.manager)continue;let g=u.get(l.manager)??[];g.push(l.key),u.set(l.manager,g)}let d=[...t.reportsTo??[]];while(d.length>0){let l=d.shift();if(!l||r.has(l))continue;r.add(l),d.push(...u.get(l)??[])}for(let l of t.people??[]){if(o.has(l))r.add(l);if(s.has(l))n.add(l)}return{workforce:r,customers:n}}function _(e){if(Array.isArray(e))return e.map(_);if(!ee(e))return e;return Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0).sort(([t],[r])=>t<r?-1:t>r?1:0).map(([t,r])=>[t,_(r)]))}function A(e){let t=structuredClone(e);return delete t.meta.contentHash,De("sha256").update(JSON.stringify(_(t)),"utf8").digest("hex")}function Le(e,t){let r=A(e);if(e.meta.contentHash&&e.meta.contentHash!==r)throw Error("dataset.meta.contentHash does not match the dataset content");return{id:e.meta.id,version:e.meta.version,hash:r,source:t}}async function xe(e){let t=f(JSON.parse(await Te(e,"utf8")),"lockfile");return{id:m(t.id,"lockfile.id"),version:m(t.version,"lockfile.version"),hash:m(t.hash,"lockfile.hash"),source:m(t.source,"lockfile.source")}}async function Ae(e,t){await Se(e,`${JSON.stringify(t,null,2)}
2
+ `,"utf8")}var re={basic:"auto",elevated:"approve",privileged:"approve",admin:"approve-with-justification"};function Ee(e,t,r){return e.apps[t]?.policies?.[r]}function Ce(e){if(e.policy?.workflow)return e.policy.workflow;if(e.riskLevel){let t=e.overrides?.get(e.riskLevel);if(t)return t;let r=(e.defaults??re)[e.riskLevel];if(r)return r}return"approve-with-justification"}function Ie(e,t,r){if(!t?.eligible)return{eligible:!0};let n=O(e,t.eligible);return(r.kind==="user"?n.workforce.has(r.key):n.customers.has(r.key))?{eligible:!0}:{eligible:!1,reason:"not_in_eligible_set"}}function Me(e,t){if(!t?.approvers)return[];return[...O(e,t.approvers).workforce]}function Ne(e,t,r){let n=[];for(let o of e.apps[t]?.assignments??[]){let s=O(e,o.members),a=o.subjectKind==="user"?[...s.workforce]:[...s.customers],i=new Set,c=[];for(let u of a){let d=o.subjectKind==="user"?r.user(u):r.customer(u);if(d)i.add(d);else c.push(u)}n.push({entitlementId:o.entitlement,subjectKind:o.subjectKind,members:[...i],unresolved:c})}return{appSlug:t,bindings:n}}import{ACCEPT_JSON as ne,CONTENT_TYPE_JSON as se}from"@ionite/core";async function _e(e){let t=new URL(`${p(e.integratorBaseUrl)}/api/tm/stage`),r=await fetch(t,{method:"POST",headers:{...ne,...se},body:JSON.stringify({audience:e.audience,integratorTenantId:e.integratorTenantId,productId:e.productId,applicationId:e.applicationId,tenantHint:e.tenantHint,ttlSeconds:e.ttlSeconds})});return D(r,"TM stage")}function je(e){let t=new URL(p(e.tenantPath??"/api/tenants"),e.appBaseUrl),r=()=>AbortSignal.timeout(e.timeoutMs??15000),n=(s)=>new URL(`${t.pathname}/integrations/${s}`,t),o=(s,a)=>fetch(n(s),{method:"POST",headers:{...ne,Authorization:`Bearer ${e.accessToken}`,...se},body:JSON.stringify(a),signal:r()});return{authorizeUrl(){let s=n("authorize");return s.searchParams.set("handle",e.handle),s.toString()},async redeem(){let s=await o("redeem",{handle:e.handle});if(![200,202,410,429].includes(s.status))throw Error(`TM redeem failed (${s.status}): ${await s.text()}`);return D(s,"TM redeem")},async exportConfig(s){let a=await o("config-export",{handle:e.handle,authorization:s});return D(a,"TM config export")}}}import{JSON_MEDIA_TYPE as ae,readJsonResponse as Je,unexpectedBodySummary as ze}from"@ionite/core";class F extends Error{constructor(e,t){super(t);this.status=e}}function L(e,t){return{tenant:e,control:t,created:0,updated:0,unchanged:0,errors:[]}}function w(e){if(Array.isArray(e))return`[${e.map(w).join(",")}]`;if(typeof e!=="object"||e===null)return JSON.stringify(e);return`{${Object.entries(e).filter(([,t])=>t!==void 0).sort(([t],[r])=>t<r?-1:t>r?1:0).map(([t,r])=>`${JSON.stringify(t)}:${w(r)}`).join(",")}}`}function J(e,t,r){let n=new Map(t.map((s)=>[r(s),s])),o={created:0,updated:0,unchanged:0};for(let s of e){let a=n.get(r(s));if(!a)o.created+=1;else if(w(a)===w(s))o.unchanged+=1;else o.updated+=1}return o}function z(e,t){e.created+=t.created,e.updated+=t.updated,e.unchanged+=t.unchanged}async function E(e){let t=typeof e==="string"?e:await e();return t.startsWith("Bearer ")?t:`Bearer ${t}`}async function h(e,t,r,n){let o=new Headers(r.headers);if(o.set("Accept",ae),r.body)o.set("Content-Type",ae);let s=await e(t,{...r,headers:o,signal:r.signal??AbortSignal.timeout(n)});if(!s.ok){let a=await ze(s);throw new F(s.status,`${r.method??"GET"} ${new URL(t).pathname} failed (${s.status})${a.excerpt?`: ${a.excerpt}`:""}`)}if(s.status===204)return;return Je(s)}function Fe(e,t){let r=P(e);for(let n of t)r=j(r,n);return r}async function Ge(e,t,r,n){let o=e.fetch??globalThis.fetch,s=`${p(e.controls.hr)}/api/${encodeURIComponent(r)}/employees`,a=await h(o,s,{},e.timeoutMs??30000),i=t.workforce.map((d)=>({...d,active:d.active??!0,avatar:d.avatar?{url:d.avatar.url}:void 0}));z(n,J(i,a.items,(d)=>d.employeeNumber));let c=J(t.orgUnits,a.orgUnits??[],(d)=>d.key),u=new Set(t.orgUnits.map((d)=>d.key));if(c.updated+=(a.orgUnits??[]).filter((d)=>!u.has(d.key)).length,z(n,c),!e.dryRun&&(n.created>0||n.updated>0)){let d={orgUnits:t.orgUnits,workforce:i};await h(o,s,{method:"PUT",body:JSON.stringify(d)},e.timeoutMs??30000)}}async function Ve(e,t,r){let n=e.fetch??globalThis.fetch,o=`${p(e.controls.iam)}/api/${encodeURIComponent(t)}/sync`,s=await E(e.authorization.iam);if(e.dryRun){let c=await h(n,o,{headers:{Authorization:s}},e.timeoutMs??30000);if(c.state==="done"&&(c.result?.errors?.length??0)===0)r.unchanged=1;else if(c.state==="error")r.errors.push(c.error??"IAM reconciliation failed");else r.errors.push(`IAM reconciliation is ${c.state}`);return}let a=e.timeoutMs??30000;await h(n,`${o}?async=true`,{method:"POST",headers:{Authorization:s}},a);let i=Date.now()+a;while(Date.now()<i){let c=await h(n,o,{headers:{Authorization:s}},a);if(c.state==="done"){if(r.errors.push(...c.result?.errors??[]),r.updated=Object.values(c.result?.appWrites??{}).reduce((u,d)=>u+d,0),r.updated===0&&r.errors.length===0)r.unchanged=1;return}if(c.state==="error"){r.errors.push(c.error??"IAM reconciliation failed");return}await new Promise((u)=>setTimeout(u,200))}r.errors.push(`IAM reconciliation timed out after ${a}ms`)}async function He(e,t,r,n){let o=e.fetch??globalThis.fetch,s=`${p(e.controls.sso)}/api/admin/tenants/${encodeURIComponent(r)}/dataset`,a=await E(e.authorization.sso),i=await h(o,s,{headers:{Authorization:a}},e.timeoutMs??30000),c={dataset:{orgUnits:t.orgUnits,workforce:t.workforce,customers:t.customers},apps:t.apps};if(w(i)===w(c))n.unchanged=1;else if(Object.keys(i.apps).length===0)n.created=1;else n.updated=1;if(!e.dryRun&&(n.created>0||n.updated>0))await h(o,s,{method:"PUT",headers:{Authorization:a},body:JSON.stringify(c)},e.timeoutMs??30000)}async function We(e,t,r,n){if(!e.controls.ciam||!e.authorization.ciam){n.skipped="control_not_configured";return}let o=e.fetch??globalThis.fetch,s=new URL(`${p(e.controls.ciam)}/api/admin/customers`);s.searchParams.set("tenant",r);let a=await E(e.authorization.ciam);try{let i=t.customers.map((k)=>({key:k.key,email:k.email,givenName:k.givenName,familyName:k.familyName})),c=await h(o,s.toString(),{headers:{Authorization:a}},e.timeoutMs??30000),u=new URL(`${p(e.controls.ciam)}/api/admin/dataset`);u.searchParams.set("tenant",r);let d=await h(o,u.toString(),{headers:{Authorization:a}},e.timeoutMs??30000),l={dataset:{orgUnits:t.orgUnits,workforce:t.workforce,customers:t.customers},apps:t.apps},g=J(i,c.items,(k)=>k.key),b=w(d)!==w(l);if(z(n,g),!b)n.unchanged+=1;else if(Object.keys(d.apps).length===0)n.created+=1;else n.updated+=1;if(!e.dryRun&&(g.created>0||g.updated>0)){let k={customers:i};await h(o,s.toString(),{method:"PUT",headers:{Authorization:a},body:JSON.stringify(k)},e.timeoutMs??30000)}if(!e.dryRun&&b)await h(o,u.toString(),{method:"PUT",headers:{Authorization:a},body:JSON.stringify(l)},e.timeoutMs??30000)}catch(i){if(i instanceof F&&(i.status===404||i.status===500&&i.message.includes("Only HTML requests are supported here"))){n.skipped="control_lacks_dataset_api";return}throw i}}async function qe(e,t,r,n){let o=e.fetch??globalThis.fetch,s=`${p(e.controls.iam)}/api/${encodeURIComponent(r)}/dataset`,a=await E(e.authorization.iam),i=await h(o,s,{headers:{Authorization:a}},e.timeoutMs??30000),c={dataset:{orgUnits:t.orgUnits,workforce:t.workforce,customers:t.customers},apps:t.apps};if(w(i)===w(c))n.unchanged=1;else if(Object.keys(i.apps).length===0)n.created=1;else n.updated=1;if(!e.dryRun&&(n.created>0||n.updated>0))await h(o,s,{method:"PUT",headers:{Authorization:a},body:JSON.stringify(c)},e.timeoutMs??30000)}async function x(e,t){try{await t()}catch(r){e.errors.push(r instanceof Error?r.message:String(r))}return e}async function Be(e){let t=P(e.dataset),r=t.meta.contentHash,n=A(t);if(r&&r!==n)throw Error(`Dataset content hash mismatch: expected ${r}, received ${n}`);let o=Fe(t,e.overlays??[]),s=[];for(let a of e.tenants){let i=L(a,"hr");s.push(await x(i,()=>Ge(e,o,a,i)));let c=L(a,"iam-sync");s.push(await x(c,()=>Ve(e,a,c)));let u=L(a,"sso");s.push(await x(u,()=>He(e,o,a,u)));let d=L(a,"ciam");s.push(await x(d,()=>We(e,o,a,d)));let l=L(a,"iam-dataset");s.push(await x(l,()=>qe(e,o,a,l)))}return{dataset:{id:o.meta.id,version:o.meta.version,hash:n},dryRun:e.dryRun??!1,ok:s.every((a)=>a.errors.length===0),stages:s}}function G(e){if(typeof e.getSetCookie==="function")return e.getSetCookie();let t=e.get("set-cookie");return t?[t]:[]}function V(e){let t=e.split(";",1)[0],r=t?.indexOf("=")??-1;if(!t||r<=0)return;return[t.slice(0,r).trim(),t.slice(r+1).trim()]}class C{#e;#t;constructor(e=globalThis.fetch){this.#e=new Map;this.#t=e}cookies(e){if(e)return new Map(this.#e.get(new URL(e).origin));let t=new Map;for(let r of this.#e.values())for(let[n,o]of r)t.set(n,o);return t}async fetch(e,t={}){let r=new URL(e),n=t.method??"GET",o=t.body;for(let s=0;s<=10;s++){let a=new Headers(t.headers),i=this.#e.get(r.origin);if(i?.size&&!a.has("Cookie"))a.set("Cookie",[...i].map(([d,l])=>`${d}=${l}`).join("; "));let c=await this.#t(r,{...t,method:n,body:o,headers:a,redirect:"manual"});if(this.#r(r.origin,c.headers),![301,302,303,307,308].includes(c.status))return c;let u=c.headers.get("location");if(!u)return c;if(r=new URL(u,r),c.status===303||(c.status===301||c.status===302)&&n==="POST")n="GET",o=void 0}throw Error(`Too many redirects while requesting ${e}`)}#r(e,t){let r=this.#e.get(e)??new Map;for(let n of G(t)){let o=V(n);if(!o)continue;let[s,a]=o;if(a===""||/(?:^|;)\s*Max-Age=0(?:;|$)/i.test(n)||/expires=Thu, 01 Jan 1970/i.test(n))r.delete(s);else r.set(s,a)}this.#e.set(e,r)}}function ie(e){let t=new Map;for(let r of G(e)){let n=V(r);if(n)t.set(n[0],n[1])}return t}function oe(e){return[...e].map(([t,r])=>`${t}=${r}`).join("; ")}async function ce(e,t,r={}){if(!e.loginPath)throw Error("Login path not configured");let n=r.redirectParam?`?redirect=${encodeURIComponent(r.redirectParam)}`:"",o=await t(`${e.loginPath}${n}`);if(o.status!==302)throw Error(`Login did not redirect: ${o.status}`);let s=o.headers.get("location");if(!s)throw Error("No redirect location from login");let a=ie(o.headers),i=new URL(s).searchParams.get("state");if(r.bogusCode){let g=new URL(s).searchParams.get("redirect_uri");if(!g||!i)throw Error("Missing redirect_uri/state on authorization URL");let b=new URL(g).pathname;return{callback:await t(`${b}?code=bogus-code&state=${encodeURIComponent(i)}`,{headers:{Cookie:oe(a)}}),state:i}}let c=new URL(s);if(r.groups&&r.groups.length>0)c.searchParams.set("groups",r.groups.join(" "));let u=await(r.externalFetch??globalThis.fetch)(c,{redirect:"manual"});if(u.status!==302)throw Error(`Auth did not redirect: ${u.status}`);let d=u.headers.get("location");if(!d)throw Error("No redirect location from auth");return{callback:await t(`${new URL(d).pathname}${new URL(d).search}`,{headers:{Cookie:oe(a)}}),state:i}}async function Ke(e,t){try{let{callback:r}=await ce(e,t);if(r.status!==302)return{cookies:new Map,success:!1,error:`Callback did not redirect: ${r.status}`};return{cookies:ie(r.headers),success:!0}}catch(r){return{cookies:new Map,success:!1,error:r instanceof Error?r.message:String(r)}}}import{ACCEPT_JSON as ue,CONTENT_TYPE_JSON as Ye,readJsonResponse as de}from"@ionite/core";function T(e,t){if(!e)throw Error(t)}async function Qe(e){let t=p(e.tenantPath??"/api/tenants"),r=e.profile??"tenantManager",n=await e.control.getTenantManagementToken(),o={...e.tenant,configLocator:e.tenant.configLocator??e.control.configLocator,webhookUrl:e.tenant.webhookUrl??e.control.webhookUrl},s=await fetch(new URL(t,e.baseUrl),{method:"POST",headers:{...ue,Authorization:`Bearer ${n}`,...Ye},body:JSON.stringify(o),signal:AbortSignal.timeout(e.readiness?.requestTimeoutMs??15000)}),a=await de(s).catch(()=>({}));if(s.status===409||a.status==="failed")throw Error(`Tenant ${o.id} provisioning failed: ${a.error??s.statusText}`);if(a.status==="action_required")throw Error(`Tenant ${o.id} provisioning requires action at ${a.actionUrl}`);if(T(s.status===200||s.status===201||s.status===202,`Expected tenant create to return 200, 201, or 202, got ${s.status}`),T(typeof a.tenantUrl==="string",`Tenant ${o.id} response must include tenantUrl`),r==="creation"){let g=p(a.tenantUrl),b=e.basePath??"/api/ionite";return await v(g,{...e.readiness,basePath:b}),{id:o.id,baseUrl:g,basePath:b,tenantUrl:a.tenantUrl}}let i=await fetch(new URL(`${t}/${encodeURIComponent(o.id)}`,e.baseUrl),{headers:{...ue,Authorization:`Bearer ${n}`},signal:AbortSignal.timeout(e.readiness?.requestTimeoutMs??15000)});T(i.status===200,`Expected tenant GET to return 200, got ${i.status}`);let c=await de(i);T(c.id===o.id,`Expected tenant GET id ${o.id}`),T(typeof c.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl"),T(typeof c.ionUrls.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl");let u=new URL(c.ionUrls.capabilitiezUrl);T(u.pathname.endsWith("/capabilitiez"),"Tenant capabilitiezUrl must end in /capabilitiez");let d=u.pathname.slice(0,-13)||"/",l=new URL(c.ionUrls.readyzUrl);return await v(u.origin,{...e.readiness,readyPath:`${l.pathname}${l.search}`}),{id:o.id,baseUrl:u.origin,basePath:d,tenantUrl:typeof c.tenantUrl==="string"?c.tenantUrl:a.tenantUrl}}function W(e){return{Accept:"application/scim+json",Authorization:`Bearer ${e}`}}function H(e,t){return`${e.replace(/\/$/,"")}${t}`}function Xe(e,t){let r=t.trim();if(!r)return String(e);try{let n=JSON.parse(r);if(!((Array.isArray(n.schemas)?n.schemas:[]).includes("urn:ietf:params:scim:api:messages:2.0:Error")||typeof n.scimType==="string"||typeof n.status==="string"))return`${e} ${r}`;let a=[String(n.status??e)];if(typeof n.scimType==="string"&&n.scimType)a.push(`scimType=${n.scimType}`);if(typeof n.attribute==="string"&&n.attribute)a.push(`attribute=${n.attribute}`);if(typeof n.detail==="string"&&n.detail)a.push(`detail=${JSON.stringify(n.detail)}`);return a.join(" ")}catch{return`${e} ${r}`}}function q(e,t,r,n){return`SCIM ${e} ${t} failed: ${Xe(r,n)}`}async function Ze(e,t,r=100,n){let o=[],s=1,a=0;while(!0){let i=new URL(e);if(i.searchParams.set("startIndex",String(s)),i.searchParams.set("count",String(r)),n)i.searchParams.set("filter",n);let c=await fetch(i,{headers:W(t)});if(!c.ok)throw Error(q("GET",`${i.origin}${i.pathname}`,c.status,await c.text()));let u=await c.json();if(!Array.isArray(u.Resources)||u.Resources.length===0)return o;if(o.push(...u.Resources),a=u.totalResults??o.length,o.length>=a)return o;s+=u.Resources.length}}function et(e,t){if(!t)return;try{return new URL(t,`${e.replace(/\/$/,"")}/`).toString()}catch{return}}async function tt(e,t){let r=H(e,"/ResourceTypes"),n=await fetch(r,{headers:W(t)});if(!n.ok)throw Error(q("GET",r,n.status,await n.text()));let o=await n.json(),a=(Array.isArray(o.Resources)?o.Resources:[]).find((i)=>i.id==="Group"||i.name==="Group");if(!a)return{supported:!1};return{supported:!0,groupsUrl:et(e,a.endpoint)??H(e,"/Groups")}}async function rt(e,t,r){return Ze(e,t,100,r?.requestableOnly?"requestable eq true":void 0)}async function nt(e,t,r){let n=H(e,`/Groups/${encodeURIComponent(r)}`),o=await fetch(n,{headers:W(t)});if(o.status===404)return null;if(!o.ok)throw Error(q("GET",n,o.status,await o.text()));return await o.json()}import{ACCEPT_JSON as le,CONTENT_TYPE_JSON as me}from"@ionite/core";async function st(e){let t=p(e.appBaseUrl),r=new C(e.fetch),n=()=>AbortSignal.timeout(e.timeoutMs??30000),o=await r.fetch(new URL(e.magicLinkPath??"/api/onboard/magic-link",t),{method:"POST",headers:{...le,...me},body:JSON.stringify(e.magicLinkBody?.(e.owner)??{email:e.owner.email}),signal:n()}),s=await D(o,"App owner magic-link creation"),a=e.readMagicLink?.(s)??(typeof s==="object"&&s!==null&&typeof s.magicLink==="string"?s.magicLink:void 0);if(!a)throw Error("App owner magic-link response did not include magicLink");let i=await r.fetch(new URL(a,t),{signal:n()});if(!i.ok)throw Error(`App owner magic-link login failed (${i.status}): ${await i.text()}`);let c=await r.fetch(new URL(e.tenantCreatePath??"/api/onboard",t),{method:"POST",headers:{...le,...me},body:JSON.stringify(e.tenantBody?.(e.tenant)??{slug:e.tenant.id,companyName:e.tenant.name}),signal:n()}),u=await D(c,"App tenant creation");return{tenantId:e.tenant.id,owner:e.owner,tenant:u,session:r}}import{ACCEPT_JSON as at,CONTENT_TYPE_JSON as I}from"@ionite/core";async function ot(e,t,r){let n=Date.now();while(Date.now()-n<r){try{if((await t(`${e}/api/readyz`,{headers:at,signal:AbortSignal.timeout(2000)})).ok)return}catch{}await new Promise((o)=>setTimeout(o,500))}throw Error(`Timed out waiting for the ECI at ${e}/api/readyz`)}async function it(e){let t=process.env.ION_CONTROLS_PORT?.trim()||"3700",r=p(e.eciBaseUrl??`http://localhost:${t}/ionite`),n=e.fetch??globalThis.fetch,o={fetch:n,timeoutMs:e.requestTimeoutMs??120000};if(await ot(r,n,e.readyTimeoutMs??300000),await S(`${r}/api/bootstrap`,{method:"POST",headers:I,body:JSON.stringify({companies:[e.company]})},{...o,label:"ECI company bootstrap"}),e.oem)await S(`${r}/api/oems`,{method:"POST",headers:I,body:JSON.stringify(e.oem)},{...o,label:"ECI OEM registration"});let s=await S(`${r}/api/products`,{method:"POST",headers:I,body:JSON.stringify(e.product)},{...o,label:"ECI product registration"});await e.onProductRegistered?.(s);let a=await S(`${r}/api/companies/${encodeURIComponent(e.company.id)}/products/${encodeURIComponent(s.product.id)}/integration`,{method:"POST",headers:I,body:JSON.stringify(e.integration)},{...o,label:"ECI tenant integration"});if(a.tenant.status==="error")throw Error(a.tenant.error??`Tenant ${e.company.id} failed to integrate`);return{registration:s,integration:a}}export{re as DEFAULT_WORKFLOWS,C as HttpSession,Ke as completeLoginFlow,A as datasetContentHash,Le as datasetLock,tt as discoverScimGroups,Ie as evaluateEligibility,rt as fetchScimGroupsFromUrl,Ee as findAppPolicy,nt as getScimGroupById,Be as loadDataset,j as mergeDatasetOverlay,Ue as parseDatasetJsonc,Oe as parseDatasetOverlayJsonc,Ne as planAssignments,Qe as provisionTenant,xe as readDatasetLock,S as requestJson,Me as resolveApprovers,O as resolveDatasetMemberSet,Q as resolveReadyPath,Ce as resolveWorkflow,ce as runLoginToCallback,st as seedAppOwner,it as seedIoniteTenant,_e as stageTmIntegration,be as startAppUnderTest,je as tmIntegratorDriver,P as validateDataset,v as waitForReady,Ae as writeDatasetLock};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionite/devtools",
3
- "version": "0.0.24-beta.20260928.18",
3
+ "version": "0.0.24-beta.20260930.19",
4
4
  "description": "Lightweight ionite seeding drivers and journey helpers for local development and end-to-end testing.",
5
5
  "author": "ionite",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -29,7 +29,8 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@ionite/core": "0.0.24-beta.20260928.18"
32
+ "@ionite/core": "0.0.24-beta.20260930.19",
33
+ "jsonc-parser": "^3.3.1"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@playwright/test": "^1.50.0"