iterate 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -31,7 +31,7 @@ Initial setup (writes auth + launcher config):
31
31
  npx iterate setup \
32
32
  --base-url https://dev-yourname-os.dev.iterate.com \
33
33
  --admin-password-env-var-name SERVICE_AUTH_TOKEN \
34
- --user-id usr_... \
34
+ --user-email dev-yourname@iterate.com \
35
35
  --repo-path managed \
36
36
  --auto-install true \
37
37
  --scope global
@@ -72,7 +72,7 @@ Config shape:
72
72
  "/absolute/workspace/path": {
73
73
  "baseUrl": "https://dev-yourname-os.dev.iterate.com",
74
74
  "adminPasswordEnvVarName": "SERVICE_AUTH_TOKEN",
75
- "userId": "usr_...",
75
+ "userEmail": "dev-yourname@iterate.com",
76
76
  "repoPath": "/absolute/path/to/iterate",
77
77
  "autoInstall": false
78
78
  }
@@ -116,7 +116,7 @@ You can pin explicitly:
116
116
  npx iterate setup \
117
117
  --base-url https://dev-yourname-os.dev.iterate.com \
118
118
  --admin-password-env-var-name SERVICE_AUTH_TOKEN \
119
- --user-id usr_... \
119
+ --user-email dev-yourname@iterate.com \
120
120
  --repo-path local \
121
121
  --auto-install false \
122
122
  --scope workspace
package/bin/iterate.js CHANGED
@@ -92,7 +92,7 @@ const SetupInput = z.object({
92
92
  .string()
93
93
  .describe(`Base URL for os API (for example https://dev-yourname-os.dev.iterate.com)`),
94
94
  adminPasswordEnvVarName: z.string().describe("Env var name containing admin password"),
95
- userId: z.string().describe("User ID to impersonate for os calls"),
95
+ userEmail: z.string().describe("User email to impersonate for os calls"),
96
96
  repoPath: z.string().describe("Path to iterate checkout (or 'local' / 'managed' shortcuts)"),
97
97
  autoInstall: z.boolean().describe("Auto install dependencies when missing"),
98
98
  scope: z.enum(["workspace", "global"]).describe("Where to store launcher config"),
@@ -101,7 +101,7 @@ const SetupInput = z.object({
101
101
  const AuthConfig = z.object({
102
102
  baseUrl: z.string(),
103
103
  adminPasswordEnvVarName: z.string(),
104
- userId: z.string(),
104
+ userEmail: z.string(),
105
105
  });
106
106
 
107
107
  /** @param {string} message */
@@ -350,6 +350,77 @@ const setCookiesToCookieHeader = (setCookies) => {
350
350
  return [...byName.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
351
351
  };
352
352
 
353
+ const impersonationUserIdCache = new Map();
354
+
355
+ /**
356
+ * @param {{
357
+ * superadminAuthClient: any;
358
+ * userEmail: string;
359
+ * baseUrl: string;
360
+ * }} options
361
+ */
362
+ const resolveImpersonationUserId = async ({ superadminAuthClient, userEmail, baseUrl }) => {
363
+ const normalizedEmail = userEmail.trim().toLowerCase();
364
+ const cacheKey = `${baseUrl}::${normalizedEmail}`;
365
+ const cachedUserId = impersonationUserIdCache.get(cacheKey);
366
+ if (cachedUserId) {
367
+ return cachedUserId;
368
+ }
369
+
370
+ /** @type {any[]} */
371
+ let users = [];
372
+
373
+ try {
374
+ const result = await superadminAuthClient.admin.listUsers({
375
+ query: {
376
+ filterField: "email",
377
+ filterOperator: "eq",
378
+ filterValue: normalizedEmail,
379
+ limit: 10,
380
+ },
381
+ fetchOptions: {
382
+ throw: true,
383
+ },
384
+ });
385
+ users = Array.isArray(result?.users) ? result.users : [];
386
+ } catch {
387
+ const result = await superadminAuthClient.admin.listUsers({
388
+ query: {
389
+ searchField: "email",
390
+ searchOperator: "contains",
391
+ searchValue: normalizedEmail,
392
+ limit: 100,
393
+ },
394
+ fetchOptions: {
395
+ throw: true,
396
+ },
397
+ });
398
+ users = Array.isArray(result?.users) ? result.users : [];
399
+ }
400
+
401
+ const exactMatches = users.filter(
402
+ (user) =>
403
+ user &&
404
+ typeof user === "object" &&
405
+ "email" in user &&
406
+ typeof user.email === "string" &&
407
+ user.email.toLowerCase() === normalizedEmail &&
408
+ "id" in user &&
409
+ typeof user.id === "string",
410
+ );
411
+
412
+ if (exactMatches.length === 0) {
413
+ throw new Error(`No user found with email ${userEmail}`);
414
+ }
415
+ if (exactMatches.length > 1) {
416
+ throw new Error(`Multiple users found with email ${userEmail}`);
417
+ }
418
+
419
+ const resolvedUserId = exactMatches[0].id;
420
+ impersonationUserIdCache.set(cacheKey, resolvedUserId);
421
+ return resolvedUserId;
422
+ };
423
+
353
424
  /** @param {z.infer<typeof AuthConfig>} authConfig */
354
425
  const authDance = async (authConfig) => {
355
426
  let superadminSetCookie;
@@ -387,9 +458,15 @@ const authDance = async (authConfig) => {
387
458
  plugins: [adminClient()],
388
459
  });
389
460
 
461
+ const userId = await resolveImpersonationUserId({
462
+ superadminAuthClient,
463
+ userEmail: authConfig.userEmail,
464
+ baseUrl: authConfig.baseUrl,
465
+ });
466
+
390
467
  let impersonateSetCookie;
391
468
  await superadminAuthClient.admin.impersonateUser({
392
- userId: authConfig.userId,
469
+ userId,
393
470
  fetchOptions: {
394
471
  throw: true,
395
472
  onResponse: (ctx) => {
@@ -607,7 +684,7 @@ const launcherProcedures = {
607
684
  workspacePatch: {
608
685
  baseUrl: input.baseUrl,
609
686
  adminPasswordEnvVarName: input.adminPasswordEnvVarName,
610
- userId: input.userId,
687
+ userEmail: input.userEmail,
611
688
  },
612
689
  scope: input.scope,
613
690
  workspacePath: process.cwd(),
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "iterate",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for iterate",
5
- "license": "AGPL-3.0-only",
5
+ "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",