@ionite/devtools 0.0.24-beta.20260917.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/dist/index.d.ts +321 -0
- package/dist/index.js +1 -0
- package/dist/playwright.d.ts +29 -0
- package/dist/playwright.js +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2026 IONITE ENTERPRISE SOFTWARE INC. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its associated documentation are closed source and
|
|
4
|
+
proprietary to IONITE ENTERPRISE SOFTWARE INC.
|
|
5
|
+
|
|
6
|
+
The published @ionite packages are licensed solely for use with ionite
|
|
7
|
+
products, under a separate written agreement or under evaluation terms
|
|
8
|
+
published by ionite. No other license is granted.
|
|
9
|
+
|
|
10
|
+
These materials do not grant any license under ionite's patents or patent
|
|
11
|
+
applications. They do not grant any right to use ionite trademarks except
|
|
12
|
+
as needed to refer to ionite products.
|
|
13
|
+
|
|
14
|
+
Documentation, including the public LLM guides, describes how to use the
|
|
15
|
+
SDK. It does not grant a right to copy, redistribute, or reimplement the
|
|
16
|
+
platform.
|
|
17
|
+
|
|
18
|
+
See https://ionite.com/legal
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
interface WaitForReadyOptions {
|
|
2
|
+
/** Explicit readiness path; otherwise `${basePath}/readyz`. */
|
|
3
|
+
readyPath?: string;
|
|
4
|
+
/** ionite mount used to derive readiness. @default "/api/ionite" */
|
|
5
|
+
basePath?: string;
|
|
6
|
+
/** Optional liveness path that must also return OK. */
|
|
7
|
+
livenessPath?: string;
|
|
8
|
+
/** @default 30 */
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
/** @default 1000 */
|
|
11
|
+
retryDelayMs?: number;
|
|
12
|
+
/** Per-request timeout. @default 3000 */
|
|
13
|
+
requestTimeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
interface StartAppOptions extends WaitForReadyOptions {
|
|
16
|
+
/** Executable to run, for example `bun`. */
|
|
17
|
+
command: string;
|
|
18
|
+
args?: string[];
|
|
19
|
+
cwd?: string;
|
|
20
|
+
/** Environment merged over `process.env`. */
|
|
21
|
+
env?: Record<string, string | undefined>;
|
|
22
|
+
/** Public URL polled for readiness and returned in the handle. */
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
}
|
|
25
|
+
interface AppUnderTest {
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
pid?: number;
|
|
28
|
+
/** SIGTERM the process tree, escalating to SIGKILL after five seconds. */
|
|
29
|
+
stop(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
declare function resolveReadyPath(options: Pick<WaitForReadyOptions, "readyPath" | "basePath">): string;
|
|
32
|
+
/** Poll an App until liveness (when configured) and ionite readiness both return OK. */
|
|
33
|
+
declare function waitForReady(baseUrl: string, options?: WaitForReadyOptions): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Spawn an App in its own POSIX process group so stop() also cleans up child workers.
|
|
36
|
+
*/
|
|
37
|
+
declare function startAppUnderTest(options: StartAppOptions): Promise<AppUnderTest>;
|
|
38
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
39
|
+
type RequestJsonOptions = {
|
|
40
|
+
fetch?: FetchLike;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
label?: string;
|
|
43
|
+
};
|
|
44
|
+
declare function requestJson<T>(url: string | URL, init: RequestInit, options?: RequestJsonOptions): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Small origin-scoped cookie session for HTTP-only development drivers.
|
|
47
|
+
*
|
|
48
|
+
* It intentionally implements only the host-only cookies used by ionite's local controls and Apps;
|
|
49
|
+
* browser policy validation remains the responsibility of Playwright and ECV.
|
|
50
|
+
*/
|
|
51
|
+
declare class HttpSession {
|
|
52
|
+
private;
|
|
53
|
+
constructor(fetch?: FetchLike);
|
|
54
|
+
cookies(origin?: string): ReadonlyMap<string, string>;
|
|
55
|
+
fetch(input: string | URL, init?: RequestInit): Promise<Response>;
|
|
56
|
+
}
|
|
57
|
+
type IoniteTenantSelection = {
|
|
58
|
+
create: {
|
|
59
|
+
slug: string;
|
|
60
|
+
companyName: string;
|
|
61
|
+
};
|
|
62
|
+
} | {
|
|
63
|
+
pick: {
|
|
64
|
+
tenantId: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
type LinkAppToIoniteOptions = {
|
|
68
|
+
appBaseUrl: string;
|
|
69
|
+
appTenantId: string;
|
|
70
|
+
ioniteBaseUrl: string;
|
|
71
|
+
ioniteTenant: IoniteTenantSelection;
|
|
72
|
+
appSession?: HttpSession;
|
|
73
|
+
ioniteSession?: HttpSession;
|
|
74
|
+
startPath?: string;
|
|
75
|
+
statusPath?: string;
|
|
76
|
+
tenantPath?: string;
|
|
77
|
+
confirmPath?: string;
|
|
78
|
+
product?: string;
|
|
79
|
+
timeoutMs?: number;
|
|
80
|
+
};
|
|
81
|
+
type LinkedIoniteTenant = {
|
|
82
|
+
status: "active";
|
|
83
|
+
ioniteTenantId: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Drive the App-owned connect handshake without encoding product-specific database or UI state.
|
|
87
|
+
*/
|
|
88
|
+
declare function linkAppToIonite(options: LinkAppToIoniteOptions): Promise<LinkedIoniteTenant>;
|
|
89
|
+
type PathFetch = (path: string, init?: RequestInit) => Promise<Response>;
|
|
90
|
+
type OidcLoginConfig = {
|
|
91
|
+
loginPath?: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Drive an OIDC authorization-code flow whose test IdP auto-authenticates.
|
|
95
|
+
*/
|
|
96
|
+
declare function runLoginToCallback(config: OidcLoginConfig, fetch: PathFetch, options?: {
|
|
97
|
+
redirectParam?: string;
|
|
98
|
+
bogusCode?: boolean;
|
|
99
|
+
externalFetch?: FetchLike;
|
|
100
|
+
}): Promise<{
|
|
101
|
+
callback: Response;
|
|
102
|
+
state: string | null;
|
|
103
|
+
}>;
|
|
104
|
+
declare function completeLoginFlow(config: OidcLoginConfig, fetch: PathFetch): Promise<{
|
|
105
|
+
cookies: Map<string, string>;
|
|
106
|
+
success: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
}>;
|
|
109
|
+
type TenantProvisioningProfile = "tenantManager" | "creation";
|
|
110
|
+
type DevtoolsTenantControl = {
|
|
111
|
+
configLocator: unknown;
|
|
112
|
+
getTenantManagementToken(): Promise<string>;
|
|
113
|
+
webhookUrl?: string;
|
|
114
|
+
};
|
|
115
|
+
type ProvisionTenantOptions = {
|
|
116
|
+
control: DevtoolsTenantControl;
|
|
117
|
+
/** Root Tenant Management App URL. */
|
|
118
|
+
baseUrl: string;
|
|
119
|
+
/** @default "/api/tenants" */
|
|
120
|
+
tenantPath?: string;
|
|
121
|
+
/** @default "tenantManager" */
|
|
122
|
+
profile?: TenantProvisioningProfile;
|
|
123
|
+
tenant: {
|
|
124
|
+
id: string;
|
|
125
|
+
configLocator?: unknown;
|
|
126
|
+
webhookUrl?: string;
|
|
127
|
+
[key: string]: unknown;
|
|
128
|
+
};
|
|
129
|
+
/** Tenant ionite mount for creation-only profiles. @default "/api/ionite" */
|
|
130
|
+
basePath?: string;
|
|
131
|
+
readiness?: WaitForReadyOptions;
|
|
132
|
+
};
|
|
133
|
+
type ProvisionedTenant = {
|
|
134
|
+
id: string;
|
|
135
|
+
baseUrl: string;
|
|
136
|
+
basePath: string;
|
|
137
|
+
tenantUrl: string;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Provision a fixture tenant through an App's real Tenant Management contract.
|
|
141
|
+
*/
|
|
142
|
+
declare function provisionTenant(options: ProvisionTenantOptions): Promise<ProvisionedTenant>;
|
|
143
|
+
type SeedAppOwnerOptions<TTenant = Record<string, unknown>> = {
|
|
144
|
+
appBaseUrl: string;
|
|
145
|
+
owner: {
|
|
146
|
+
email: string;
|
|
147
|
+
name?: string;
|
|
148
|
+
};
|
|
149
|
+
tenant: {
|
|
150
|
+
id: string;
|
|
151
|
+
name: string;
|
|
152
|
+
};
|
|
153
|
+
magicLinkPath?: string;
|
|
154
|
+
tenantCreatePath?: string;
|
|
155
|
+
magicLinkBody?: (owner: SeedAppOwnerOptions<TTenant>["owner"]) => unknown;
|
|
156
|
+
tenantBody?: (tenant: SeedAppOwnerOptions<TTenant>["tenant"]) => unknown;
|
|
157
|
+
readMagicLink?: (response: unknown) => string;
|
|
158
|
+
fetch?: FetchLike;
|
|
159
|
+
timeoutMs?: number;
|
|
160
|
+
};
|
|
161
|
+
type SeededAppOwner<TTenant> = {
|
|
162
|
+
tenantId: string;
|
|
163
|
+
owner: {
|
|
164
|
+
email: string;
|
|
165
|
+
name?: string;
|
|
166
|
+
};
|
|
167
|
+
tenant: TTenant;
|
|
168
|
+
session: HttpSession;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Create an App owner through its real root-CIAM magic-link and tenant-create surfaces.
|
|
172
|
+
*
|
|
173
|
+
* Endpoint and payload adapters are explicit because onboarding is App-owned; devtools never
|
|
174
|
+
* guesses an App's customer schema or silently writes its stores.
|
|
175
|
+
*/
|
|
176
|
+
declare function seedAppOwner<TTenant = Record<string, unknown>>(options: SeedAppOwnerOptions<TTenant>): Promise<SeededAppOwner<TTenant>>;
|
|
177
|
+
import { EnvironmentType } from "@ionite/core";
|
|
178
|
+
type DevControlModule = "sso" | "ciam" | "iam" | "otel" | "authz";
|
|
179
|
+
type DevProductRegistration = {
|
|
180
|
+
slug: string;
|
|
181
|
+
name?: string;
|
|
182
|
+
oemId?: string;
|
|
183
|
+
ownerCompanyId?: string;
|
|
184
|
+
urlStyle?: "root" | "multi-tenant";
|
|
185
|
+
baseUrl: string;
|
|
186
|
+
reachBaseUrl?: string;
|
|
187
|
+
configType?: "websocket" | "lfv" | "http" | "mirror";
|
|
188
|
+
isControl?: boolean;
|
|
189
|
+
tenantManaged?: boolean;
|
|
190
|
+
tenantConfigDelivery?: "direct" | "lfv-mirror";
|
|
191
|
+
root?: {
|
|
192
|
+
ciamProviders?: string[];
|
|
193
|
+
cookiesSecure?: boolean;
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
type DevIntegrationIntent = {
|
|
197
|
+
controls?: DevControlModule[];
|
|
198
|
+
workloadCalls?: string[];
|
|
199
|
+
ciamProviders?: string[];
|
|
200
|
+
iam?: {
|
|
201
|
+
groups?: boolean;
|
|
202
|
+
delta?: "patch";
|
|
203
|
+
customers?: {
|
|
204
|
+
assignment?: "app" | "iam" | "iam-request";
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
oaa?: {
|
|
208
|
+
applicationName: string;
|
|
209
|
+
datasourceId?: string;
|
|
210
|
+
};
|
|
211
|
+
cookiesSecure?: boolean;
|
|
212
|
+
environmentType?: EnvironmentType;
|
|
213
|
+
};
|
|
214
|
+
type RegisteredDevProduct = {
|
|
215
|
+
product: {
|
|
216
|
+
id: string;
|
|
217
|
+
slug: string;
|
|
218
|
+
name: string;
|
|
219
|
+
};
|
|
220
|
+
env: Record<string, string>;
|
|
221
|
+
};
|
|
222
|
+
type SeededIoniteTenant = {
|
|
223
|
+
registration: RegisteredDevProduct;
|
|
224
|
+
integration: {
|
|
225
|
+
tenant: {
|
|
226
|
+
companyId: string;
|
|
227
|
+
productId: string;
|
|
228
|
+
status: "provisioning" | "ready" | "error";
|
|
229
|
+
error?: string;
|
|
230
|
+
};
|
|
231
|
+
configPath: string;
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
type SeedIoniteTenantOptions = {
|
|
235
|
+
eciBaseUrl?: string;
|
|
236
|
+
company: {
|
|
237
|
+
id: string;
|
|
238
|
+
name?: string;
|
|
239
|
+
controls?: string[];
|
|
240
|
+
};
|
|
241
|
+
oem?: {
|
|
242
|
+
id: string;
|
|
243
|
+
name: string;
|
|
244
|
+
};
|
|
245
|
+
product: DevProductRegistration;
|
|
246
|
+
integration: DevIntegrationIntent;
|
|
247
|
+
/**
|
|
248
|
+
* Start or reconfigure the App with the returned root environment before ECI calls its TM API.
|
|
249
|
+
*/
|
|
250
|
+
onProductRegistered?: (registration: RegisteredDevProduct) => Promise<void> | void;
|
|
251
|
+
fetch?: FetchLike;
|
|
252
|
+
readyTimeoutMs?: number;
|
|
253
|
+
requestTimeoutMs?: number;
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Seed one product tenant through the ECI's public HTTP intent API.
|
|
257
|
+
*
|
|
258
|
+
* The callback is the lifecycle boundary: product registration returns root configuration, the
|
|
259
|
+
* caller starts the App, and only then may ECI invoke the App's Tenant Management endpoint.
|
|
260
|
+
*/
|
|
261
|
+
declare function seedIoniteTenant(options: SeedIoniteTenantOptions): Promise<SeededIoniteTenant>;
|
|
262
|
+
type WorkforceUserFixture = {
|
|
263
|
+
key: string;
|
|
264
|
+
externalId?: string;
|
|
265
|
+
userName: string;
|
|
266
|
+
displayName?: string;
|
|
267
|
+
active?: boolean;
|
|
268
|
+
emails?: Array<{
|
|
269
|
+
value: string;
|
|
270
|
+
primary?: boolean;
|
|
271
|
+
type?: string;
|
|
272
|
+
}>;
|
|
273
|
+
name?: {
|
|
274
|
+
givenName?: string;
|
|
275
|
+
familyName?: string;
|
|
276
|
+
formatted?: string;
|
|
277
|
+
};
|
|
278
|
+
attributes?: Record<string, unknown>;
|
|
279
|
+
};
|
|
280
|
+
type WorkforceGroupFixture = {
|
|
281
|
+
key: string;
|
|
282
|
+
externalId?: string;
|
|
283
|
+
displayName: string;
|
|
284
|
+
attributes?: Record<string, unknown>;
|
|
285
|
+
};
|
|
286
|
+
type WorkforceMembershipFixture = {
|
|
287
|
+
group: string;
|
|
288
|
+
users: string[];
|
|
289
|
+
};
|
|
290
|
+
type WorkforceFixtureV1 = {
|
|
291
|
+
version: 1;
|
|
292
|
+
users: WorkforceUserFixture[];
|
|
293
|
+
groups: WorkforceGroupFixture[];
|
|
294
|
+
memberships?: WorkforceMembershipFixture[];
|
|
295
|
+
};
|
|
296
|
+
type SeedWorkforceOptions = {
|
|
297
|
+
appBaseUrl: string;
|
|
298
|
+
scimPath?: string;
|
|
299
|
+
fixture: WorkforceFixtureV1;
|
|
300
|
+
authorization: string | (() => Promise<string>);
|
|
301
|
+
/**
|
|
302
|
+
* Materialize App-owned employee records before SCIM adds IAM metadata. Required when the App,
|
|
303
|
+
* like G1, is the workforce system of record and deliberately rejects SCIM-created users.
|
|
304
|
+
*/
|
|
305
|
+
prepareUsers?: (users: readonly WorkforceUserFixture[]) => Promise<void>;
|
|
306
|
+
fetch?: FetchLike;
|
|
307
|
+
timeoutMs?: number;
|
|
308
|
+
};
|
|
309
|
+
type SeededWorkforce = {
|
|
310
|
+
users: ReadonlyMap<string, string>;
|
|
311
|
+
groups: ReadonlyMap<string, string>;
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Replay a versioned workforce fixture against an App's SCIM surface.
|
|
315
|
+
*
|
|
316
|
+
* Stages preserve referential integrity while parallelizing independent writes: users, groups, then
|
|
317
|
+
* membership replacement. Re-running the same fixture updates rows by externalId instead of
|
|
318
|
+
* duplicating them.
|
|
319
|
+
*/
|
|
320
|
+
declare function seedWorkforce(options: SeedWorkforceOptions): Promise<SeededWorkforce>;
|
|
321
|
+
export { AppUnderTest, DevControlModule, DevIntegrationIntent, DevProductRegistration, DevtoolsTenantControl, FetchLike, HttpSession, IoniteTenantSelection, LinkAppToIoniteOptions, LinkedIoniteTenant, OidcLoginConfig, PathFetch, ProvisionTenantOptions, ProvisionedTenant, RegisteredDevProduct, RequestJsonOptions, SeedAppOwnerOptions, SeedIoniteTenantOptions, SeedWorkforceOptions, SeededAppOwner, SeededIoniteTenant, SeededWorkforce, StartAppOptions, TenantProvisioningProfile, WaitForReadyOptions, WorkforceFixtureV1, WorkforceGroupFixture, WorkforceMembershipFixture, WorkforceUserFixture, completeLoginFlow, linkAppToIonite, provisionTenant, requestJson, resolveReadyPath, runLoginToCallback, seedAppOwner, seedIoniteTenant, seedWorkforce, startAppUnderTest, waitForReady };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as A}from"node:child_process";function g(e){return e.replace(/\/+$/,"")}async function y(e,t,r={}){let n=await(r.fetch??globalThis.fetch)(e,{...t,signal:t.signal??AbortSignal.timeout(r.timeoutMs??30000)});if(!n.ok){let i=await n.text();throw Error(`${r.label??`${t.method??"GET"} ${e}`} failed (${n.status}): ${i}`)}if(n.status===204)return;return await n.json()}var F="/api/ionite",N=5000,_=2000;function W(e){return new Promise((t)=>setTimeout(t,e))}function R(e,t){if(e.exitCode!==null||e.signalCode!==null)return Promise.resolve(!0);return new Promise((r)=>{let n=!1,i=()=>{if(n)return;n=!0,clearTimeout(o),r(!0)},o=setTimeout(()=>{if(n)return;n=!0,e.removeListener("exit",i),r(!1)},t);e.once("exit",i)})}function L(e){if(e.readyPath)return e.readyPath;return`${g(e.basePath??F)}/readyz`}async function T(e,t={}){let{livenessPath:r,maxRetries:n=30,retryDelayMs:i=1000,requestTimeoutMs:o=3000}=t,d=L(t),c;for(let p=0;p<n;p++){try{if(r){let l=await fetch(new URL(r,e),{signal:AbortSignal.timeout(o)});if(!l.ok)throw Error(`liveness returned ${l.status}`)}let a=await fetch(new URL(d,e),{signal:AbortSignal.timeout(o)});if(a.ok)return;c=Error(`readiness returned ${a.status}`)}catch(a){c=a}if(p<n-1)await W(i)}let s=c instanceof Error?c.message:String(c);throw Error(`App not ready at ${new URL(d,e)} after ${n} attempts: ${s}`)}async function q(e){let{command:t,args:r=[],cwd:n,env:i,baseUrl:o,...d}=e,c=process.platform!=="win32",s=A(t,r,{cwd:n,env:{...process.env,...i},stdio:"inherit",detached:c});s.on("error",(u)=>{console.error(`Failed to start app under test (${t}):`,u)});let p=(u)=>{try{if(c&&s.pid!==void 0)process.kill(-s.pid,u);else s.kill(u)}catch{}},a=()=>p("SIGKILL");process.once("exit",a);let l=async()=>{if(s.exitCode===null&&s.signalCode===null){if(p("SIGTERM"),!await R(s,N))p("SIGKILL"),await R(s,_)}p("SIGKILL"),process.removeListener("exit",a)};try{await T(o,d)}catch(u){throw await l(),u}return{baseUrl:o,...s.pid===void 0?{}:{pid:s.pid},stop:l}}function z(e){if(typeof e.getSetCookie==="function")return e.getSetCookie();let t=e.get("set-cookie");return t?[t]:[]}function D(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 f{#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,i]of r)t.set(n,i);return t}async fetch(e,t={}){let r=new URL(e),n=t.method??"GET",i=t.body;for(let o=0;o<=10;o++){let d=new Headers(t.headers),c=this.#e.get(r.origin);if(c?.size&&!d.has("Cookie"))d.set("Cookie",[...c].map(([a,l])=>`${a}=${l}`).join("; "));let s=await this.#t(r,{...t,method:n,body:i,headers:d,redirect:"manual"});if(this.#n(r.origin,s.headers),![301,302,303,307,308].includes(s.status))return s;let p=s.headers.get("location");if(!p)return s;if(r=new URL(p,r),s.status===303||(s.status===301||s.status===302)&&n==="POST")n="GET",i=void 0}throw Error(`Too many redirects while requesting ${e}`)}#n(e,t){let r=this.#e.get(e)??new Map;for(let n of z(t)){let i=D(n);if(!i)continue;let[o,d]=i;if(d===""||/(?:^|;)\s*Max-Age=0(?:;|$)/i.test(n)||/expires=Thu, 01 Jan 1970/i.test(n))r.delete(o);else r.set(o,d)}this.#e.set(e,r)}}async function b(e,t){if(!e.ok)throw Error(`${t} failed (${e.status}): ${await e.text()}`);return await e.json()}async function B(e){let t=g(e.appBaseUrl),r=g(e.ioniteBaseUrl),n=e.appSession??new f,i=e.ioniteSession??new f,o=()=>AbortSignal.timeout(e.timeoutMs??30000),d=await n.fetch(new URL(e.startPath??"/api/integrations/ionite/start",t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tenantId:e.appTenantId,ioniteBaseUrl:r}),signal:o()}),c=await b(d,"Connect start"),p=new URL(c.connectUrl).searchParams,a;if("create"in e.ioniteTenant){let w=await i.fetch(new URL(e.tenantPath??"/api/connect/tenants",r),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.ioniteTenant.create),signal:o()});a=(await b(w,"ionite console tenant creation")).tenantId}else a=e.ioniteTenant.pick.tenantId;let l=await i.fetch(new URL(e.confirmPath??"/api/connect/confirm",r),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ioniteTenantId:a,product:p.get("product")??e.product??"",appUrl:p.get("app_url")??t,appTenantId:p.get("app_tenant")??e.appTenantId,state:p.get("state")??c.state,code:p.get("code")??"",returnUrl:p.get("return_url")??""}),signal:o()}),u=await b(l,"Connect confirmation");if(u.redirectTo?.includes("result=error")||u.error)throw Error(u.error??"Connect confirmation returned an error result");let m=p.get("return_url");if(m){let w=new URL(m);w.searchParams.set("state",c.state),w.searchParams.set("result","success"),w.searchParams.set("ionite_tenant",a);let x=await n.fetch(w,{signal:o()});if(!x.ok)throw Error(`Connect completion failed (${x.status}): ${await x.text()}`)}let k=new URL(e.statusPath??"/api/integrations/ionite",t);k.searchParams.set("tenant",e.appTenantId);let P=await n.fetch(k,{signal:o()}),U=await b(P,"Connect status");if(U.status!=="active"||!U.ioniteTenantId)throw Error(`Connect link is not active: ${JSON.stringify(U)}`);return{status:"active",ioniteTenantId:U.ioniteTenantId}}function j(e){return typeof e.getSetCookie==="function"?e.getSetCookie():e.get("set-cookie")?[e.get("set-cookie")]:[]}function v(e){let t=new Map;for(let r of j(e)){let n=r.split(";",1)[0],i=n?.indexOf("=")??-1;if(!n||i<=0)continue;t.set(n.slice(0,i).trim(),n.slice(i+1).trim())}return t}function I(e){return[...e].map(([t,r])=>`${t}=${r}`).join("; ")}async function O(e,t,r={}){if(!e.loginPath)throw Error("Login path not configured");let n=r.redirectParam?`?redirect=${encodeURIComponent(r.redirectParam)}`:"",i=await t(`${e.loginPath}${n}`);if(i.status!==302)throw Error(`Login did not redirect: ${i.status}`);let o=i.headers.get("location");if(!o)throw Error("No redirect location from login");let d=v(i.headers),c=new URL(o).searchParams.get("state");if(r.bogusCode){let l=new URL(o).searchParams.get("redirect_uri");if(!l||!c)throw Error("Missing redirect_uri/state on authorization URL");let u=new URL(l).pathname;return{callback:await t(`${u}?code=bogus-code&state=${encodeURIComponent(c)}`,{headers:{Cookie:I(d)}}),state:c}}let s=await(r.externalFetch??globalThis.fetch)(o,{redirect:"manual"});if(s.status!==302)throw Error(`Auth did not redirect: ${s.status}`);let p=s.headers.get("location");if(!p)throw Error("No redirect location from auth");return{callback:await t(`${new URL(p).pathname}${new URL(p).search}`,{headers:{Cookie:I(d)}}),state:c}}async function G(e,t){try{let{callback:r}=await O(e,t);if(r.status!==302)return{cookies:new Map,success:!1,error:`Callback did not redirect: ${r.status}`};return{cookies:v(r.headers),success:!0}}catch(r){return{cookies:new Map,success:!1,error:r instanceof Error?r.message:String(r)}}}function h(e,t){if(!e)throw Error(t)}async function J(e){let t=g(e.tenantPath??"/api/tenants"),r=e.profile??"tenantManager",n=await e.control.getTenantManagementToken(),i={...e.tenant,configLocator:e.tenant.configLocator??e.control.configLocator,webhookUrl:e.tenant.webhookUrl??e.control.webhookUrl},o=await fetch(new URL(t,e.baseUrl),{method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"},body:JSON.stringify(i),signal:AbortSignal.timeout(e.readiness?.requestTimeoutMs??15000)}),d=await o.json().catch(()=>({}));if(o.status===409||d.status==="failed")throw Error(`Tenant ${i.id} provisioning failed: ${d.error??o.statusText}`);if(d.status==="action_required")throw Error(`Tenant ${i.id} provisioning requires action at ${d.actionUrl}`);if(h(o.status===200||o.status===201||o.status===202,`Expected tenant create to return 200, 201, or 202, got ${o.status}`),h(typeof d.tenantUrl==="string",`Tenant ${i.id} response must include tenantUrl`),r==="creation"){let u=g(d.tenantUrl),m=e.basePath??"/api/ionite";return await T(u,{...e.readiness,basePath:m}),{id:i.id,baseUrl:u,basePath:m,tenantUrl:d.tenantUrl}}let c=await fetch(new URL(`${t}/${encodeURIComponent(i.id)}`,e.baseUrl),{headers:{Authorization:`Bearer ${n}`},signal:AbortSignal.timeout(e.readiness?.requestTimeoutMs??15000)});h(c.status===200,`Expected tenant GET to return 200, got ${c.status}`);let s=await c.json();h(s.id===i.id,`Expected tenant GET id ${i.id}`),h(typeof s.ionUrls?.capabilitiezUrl==="string","Tenant GET must include ionUrls.capabilitiezUrl"),h(typeof s.ionUrls.readyzUrl==="string","Tenant GET must include ionUrls.readyzUrl");let p=new URL(s.ionUrls.capabilitiezUrl);h(p.pathname.endsWith("/capabilitiez"),"Tenant capabilitiezUrl must end in /capabilitiez");let a=p.pathname.slice(0,-13)||"/",l=new URL(s.ionUrls.readyzUrl);return await T(p.origin,{...e.readiness,readyPath:`${l.pathname}${l.search}`}),{id:i.id,baseUrl:p.origin,basePath:a,tenantUrl:typeof s.tenantUrl==="string"?s.tenantUrl:d.tenantUrl}}async function C(e,t){if(!e.ok)throw Error(`${t} failed (${e.status}): ${await e.text()}`);return await e.json()}async function H(e){let t=g(e.appBaseUrl),r=new f(e.fetch),n=()=>AbortSignal.timeout(e.timeoutMs??30000),i=await r.fetch(new URL(e.magicLinkPath??"/api/onboard/magic-link",t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.magicLinkBody?.(e.owner)??{email:e.owner.email}),signal:n()}),o=await C(i,"App owner magic-link creation"),d=e.readMagicLink?.(o)??(typeof o==="object"&&o!==null&&typeof o.magicLink==="string"?o.magicLink:void 0);if(!d)throw Error("App owner magic-link response did not include magicLink");let c=await r.fetch(new URL(d,t),{signal:n()});if(!c.ok)throw Error(`App owner magic-link login failed (${c.status}): ${await c.text()}`);let s=await r.fetch(new URL(e.tenantCreatePath??"/api/onboard",t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.tenantBody?.(e.tenant)??{slug:e.tenant.id,companyName:e.tenant.name}),signal:n()}),p=await C(s,"App tenant creation");return{tenantId:e.tenant.id,owner:e.owner,tenant:p,session:r}}async function K(e,t,r){let n=Date.now();while(Date.now()-n<r){try{if((await t(`${e}/api/readyz`,{signal:AbortSignal.timeout(2000)})).ok)return}catch{}await new Promise((i)=>setTimeout(i,500))}throw Error(`Timed out waiting for the ECI at ${e}/api/readyz`)}async function V(e){let t=g(e.eciBaseUrl??"http://localhost:3500"),r=e.fetch??globalThis.fetch,n={fetch:r,timeoutMs:e.requestTimeoutMs??120000};if(await K(t,r,e.readyTimeoutMs??300000),await y(`${t}/api/bootstrap`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({companies:[e.company]})},{...n,label:"ECI company bootstrap"}),e.oem)await y(`${t}/api/oems`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.oem)},{...n,label:"ECI OEM registration"});let i=await y(`${t}/api/products`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.product)},{...n,label:"ECI product registration"});await e.onProductRegistered?.(i);let o=await y(`${t}/api/companies/${encodeURIComponent(e.company.id)}/products/${encodeURIComponent(i.product.id)}/integration`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e.integration)},{...n,label:"ECI tenant integration"});if(o.tenant.status==="error")throw Error(o.tenant.error??`Tenant ${e.company.id} failed to integrate`);return{registration:i,integration:o}}var Q="urn:ietf:params:scim:schemas:core:2.0:User",E="urn:ietf:params:scim:schemas:core:2.0:Group";function X(e){return e.replaceAll("\\","\\\\").replaceAll('"',"\\\"")}function Y(e){if(e.version!==1)throw Error(`Unsupported workforce fixture version: ${e.version}`);let t=new Set,r=new Set;for(let n of e.users){if(!n.key||!n.userName)throw Error("Every workforce user requires key and userName");if(t.has(n.key))throw Error(`Duplicate workforce user key: ${n.key}`);t.add(n.key)}for(let n of e.groups){if(!n.key||!n.displayName)throw Error("Every workforce group requires key and displayName");if(r.has(n.key))throw Error(`Duplicate workforce group key: ${n.key}`);r.add(n.key)}for(let n of e.memberships??[]){if(!r.has(n.group))throw Error(`Unknown workforce group key: ${n.group}`);for(let i of n.users)if(!t.has(i))throw Error(`Unknown workforce user key: ${i}`)}}async function M(e,t,r,n,i){let o=await e(t,{...n,headers:{Authorization:r,Accept:"application/scim+json",...n.body?{"Content-Type":"application/scim+json"}:{},...n.headers},signal:n.signal??AbortSignal.timeout(i)});if(!o.ok)throw Error(`SCIM ${n.method??"GET"} ${t.pathname} failed (${o.status}): ${await o.text()}`);return o}async function S(e,t,r,n,i,o,d){let c=new URL(`${t.pathname.replace(/\/$/,"")}/${r}`,t);c.searchParams.set("filter",`externalId eq "${X(n)}"`);let p=(await(await M(e,c,o,{},d)).json()).Resources?.[0]?.id,a=typeof p==="string"?new URL(`${c.pathname}/${encodeURIComponent(p)}`,c):c,u=await(await M(e,a,o,{method:typeof p==="string"?"PUT":"POST",body:JSON.stringify(i)},d)).json();if(typeof u.id!=="string")throw Error(`SCIM ${r} upsert response did not include id`);return u.id}async function Z(e){Y(e.fixture);let t=e.fetch??globalThis.fetch,r=e.timeoutMs??30000,n=typeof e.authorization==="string"?e.authorization:await e.authorization(),i=n.startsWith("Bearer ")?n:`Bearer ${n}`,o=e.scimPath??"/api/ionite/iam",d=new URL(`${g(o)}/`,e.appBaseUrl),c=new Map,s=new Map;await e.prepareUsers?.(e.fixture.users),await Promise.all(e.fixture.users.map(async(a)=>{let l=a.externalId??a.key,u=await S(t,d,"Users",l,{schemas:[Q],externalId:l,userName:a.userName,displayName:a.displayName,active:a.active??!0,emails:a.emails,name:a.name,...a.attributes},i,r);c.set(a.key,u)})),await Promise.all(e.fixture.groups.map(async(a)=>{let l=a.externalId??a.key,u=await S(t,d,"Groups",l,{schemas:[E],externalId:l,displayName:a.displayName,members:[],...a.attributes},i,r);s.set(a.key,u)}));let p=new Map;for(let a of e.fixture.memberships??[]){let l=p.get(a.group)??new Set;for(let u of a.users)l.add(u);p.set(a.group,l)}return await Promise.all(e.fixture.groups.map(async(a)=>{if(!s.get(a.key))throw Error(`Missing seeded group id for ${a.key}`);let m=[...p.get(a.key)??new Set].map((k)=>{let P=c.get(k);if(!P)throw Error(`Missing seeded user id for ${k}`);return{value:P,type:"User"}});await S(t,d,"Groups",a.externalId??a.key,{schemas:[E],externalId:a.externalId??a.key,displayName:a.displayName,members:m,...a.attributes},i,r)})),{users:c,groups:s}}export{f as HttpSession,G as completeLoginFlow,B as linkAppToIonite,J as provisionTenant,y as requestJson,L as resolveReadyPath,O as runLoginToCallback,H as seedAppOwner,V as seedIoniteTenant,Z as seedWorkforce,q as startAppUnderTest,T as waitForReady};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page } from "@playwright/test";
|
|
2
|
+
type SsoControlLoginOptions = {
|
|
3
|
+
loginUrl: string;
|
|
4
|
+
email: string;
|
|
5
|
+
expectedUrl?: string | RegExp;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Log in through the ionite SSO control's searchable dev directory.
|
|
10
|
+
*/
|
|
11
|
+
declare function loginWithSsoControl(page: Page, options: SsoControlLoginOptions): Promise<void>;
|
|
12
|
+
type CiamControlLoginOptions = {
|
|
13
|
+
appBaseUrl: string;
|
|
14
|
+
ciamBaseUrl: string;
|
|
15
|
+
email: string;
|
|
16
|
+
provider?: string;
|
|
17
|
+
providersPath?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Establish a customer session through the CIAM control without spending browser time on setup.
|
|
21
|
+
*/
|
|
22
|
+
declare function loginWithCiamControl(request: APIRequestContext, options: CiamControlLoginOptions): Promise<{
|
|
23
|
+
loginUrl: string;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Create a clean browser context so allow/deny assertions never inherit an owner cookie.
|
|
27
|
+
*/
|
|
28
|
+
declare function createIsolatedBrowserContext(browser: Browser, options?: BrowserContextOptions): Promise<BrowserContext>;
|
|
29
|
+
export { CiamControlLoginOptions, SsoControlLoginOptions, createIsolatedBrowserContext, loginWithCiamControl, loginWithSsoControl };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function a(r,t){if(!r.ok())throw Error(`${t} failed (${r.status()}): ${await r.text()}`)}async function c(r,t){let i=t.timeoutMs??30000;await r.goto(t.loginUrl),await r.getByRole("combobox",{name:/email/i}).fill(t.email);let e=r.getByRole("option").filter({hasText:t.email}).first();if(await e.waitFor({state:"visible",timeout:i}),await e.click(),await r.getByRole("button",{name:/sign in/i}).click(),t.expectedUrl)await r.waitForURL(t.expectedUrl,{timeout:i})}async function m(r,t){let i=await r.get(new URL(t.providersPath??"/api/ionite/auth/customer/providers",t.appBaseUrl).toString());await a(i,"CIAM provider discovery");let o=await i.json(),n=(t.provider?o.providers?.find((s)=>s.id===t.provider||s.name===t.provider):o.providers?.[0])?.loginUrl;if(!n)throw Error("CIAM provider discovery returned no loginUrl");let l=await r.post(new URL("/api/customers/login",t.ciamBaseUrl).toString(),{form:{email:t.email}});await a(l,"CIAM control login");let g=await r.get(n);return await a(g,"App customer login"),{loginUrl:n}}function p(r,t={}){return r.newContext({...t,storageState:t.storageState??{cookies:[],origins:[]}})}export{p as createIsolatedBrowserContext,m as loginWithCiamControl,c as loginWithSsoControl};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ionite/devtools",
|
|
3
|
+
"version": "0.0.24-beta.20260917.4",
|
|
4
|
+
"description": "Lightweight ionite seeding drivers and journey helpers for local development and end-to-end testing.",
|
|
5
|
+
"author": "ionite",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.6"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"./playwright": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/playwright.d.ts",
|
|
26
|
+
"default": "./dist/playwright.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@ionite/core": "0.0.24-beta.20260917.4"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@playwright/test": "^1.50.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@playwright/test": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@playwright/test": "^1.61.1",
|
|
44
|
+
"@types/node": "^26.1.0"
|
|
45
|
+
}
|
|
46
|
+
}
|