@granular-software/sdk 0.4.51 → 0.4.52

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.
@@ -14192,7 +14192,8 @@ function normalizeEnvironmentSetupSummary(setup) {
14192
14192
  environmentId: String(setup.environmentId || ""),
14193
14193
  sandboxId: String(setup.sandboxId || ""),
14194
14194
  subjectId: String(setup.subjectId || ""),
14195
- triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
14195
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : setup.triggerReason === "explicit_reset" ? "explicit_reset" : "new_environment",
14196
+ operationKey: typeof setup.operationKey === "string" ? setup.operationKey : null,
14196
14197
  lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
14197
14198
  stage: typeof setup.stage === "string" ? setup.stage : null,
14198
14199
  totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
@@ -15474,6 +15475,7 @@ var Environment = class _Environment {
15474
15475
  records: recordsToImport,
15475
15476
  batchSize: options.batchSize,
15476
15477
  setupRunId: options.setupRunId,
15478
+ operationKey: options.operationKey,
15477
15479
  writeMode: options.writeMode
15478
15480
  })
15479
15481
  }
@@ -16278,6 +16280,107 @@ var Granular = class _Granular {
16278
16280
  await this.maybeRunEnvironmentImporter(resolved, environment);
16279
16281
  return environment;
16280
16282
  }
16283
+ /**
16284
+ * Read the active environment selected by Granular for an already-recorded
16285
+ * external user. This is intentionally read-only: browser/login code must
16286
+ * not create subjects or environments as a side effect.
16287
+ */
16288
+ async getActiveEnvironmentForUser(options) {
16289
+ const sandboxId = options.sandboxId.trim();
16290
+ const tagName = options.tag.trim();
16291
+ const userId = options.userId.trim();
16292
+ if (!sandboxId || !tagName || !userId) {
16293
+ throw new Error(
16294
+ "getActiveEnvironmentForUser() requires sandboxId, tag, and userId."
16295
+ );
16296
+ }
16297
+ const subjects = await this.request(
16298
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16299
+ );
16300
+ const subject = (subjects.items || []).find(
16301
+ (item) => item.identityId === userId || item.userId === userId
16302
+ );
16303
+ if (!subject?.subjectId && !subject?.granularId) {
16304
+ return null;
16305
+ }
16306
+ const subjectId = subject.subjectId || subject.granularId;
16307
+ const tags = await this.request(
16308
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`
16309
+ );
16310
+ const tag = (tags.items || []).find(
16311
+ (item) => item?.name === tagName
16312
+ );
16313
+ if (!tag) return null;
16314
+ const query = new URLSearchParams({
16315
+ tagId: tag.tagId,
16316
+ slot: options.slot?.trim() || "default"
16317
+ });
16318
+ try {
16319
+ const payload = await this.request(
16320
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/subjects/${encodeURIComponent(subjectId)}/active-environment?${query.toString()}`
16321
+ );
16322
+ return payload.environment ? this.bindEnvironmentHandle(
16323
+ normalizeEnvironmentData(payload.environment)
16324
+ ) : null;
16325
+ } catch (error) {
16326
+ const message = error instanceof Error ? error.message : String(error);
16327
+ if (message.includes("404") || message.includes("not found")) {
16328
+ return null;
16329
+ }
16330
+ throw error;
16331
+ }
16332
+ }
16333
+ /**
16334
+ * Register one reviewed, pre-existing environment as the active workspace
16335
+ * for an external user. This is for a controlled migration only: it does
16336
+ * not create an environment and it does not run an importer.
16337
+ */
16338
+ async adoptEnvironmentForUser(options) {
16339
+ const sandboxId = options.sandboxId.trim();
16340
+ const tagName = options.tag.trim();
16341
+ const userId = options.userId.trim();
16342
+ const environmentId = options.environmentId.trim();
16343
+ if (!sandboxId || !tagName || !userId || !environmentId) {
16344
+ throw new Error(
16345
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
16346
+ );
16347
+ }
16348
+ const subjects = await this.request(
16349
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16350
+ );
16351
+ const subject = (subjects.items || []).find(
16352
+ (item) => item.identityId === userId || item.userId === userId
16353
+ );
16354
+ if (!subject?.subjectId && !subject?.granularId) {
16355
+ throw new Error(`No Granular subject exists for user ${userId}.`);
16356
+ }
16357
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16358
+ const tag = (tags.items || []).find(
16359
+ (item) => item?.name === tagName
16360
+ );
16361
+ if (!tag) {
16362
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
16363
+ }
16364
+ const payload = await this.request(
16365
+ "/control/environment-activations/adopt",
16366
+ {
16367
+ method: "POST",
16368
+ body: JSON.stringify({
16369
+ environmentId,
16370
+ subjectId: subject.subjectId || subject.granularId,
16371
+ tagId: tag.tagId,
16372
+ slot: options.slot?.trim() || "default",
16373
+ confirmExistingData: true
16374
+ })
16375
+ }
16376
+ );
16377
+ if (!payload.environment) {
16378
+ throw new Error("Granular did not return the adopted environment.");
16379
+ }
16380
+ return this.bindEnvironmentHandle(
16381
+ normalizeEnvironmentData(payload.environment)
16382
+ );
16383
+ }
16281
16384
  /**
16282
16385
  * Deprecated compatibility alias for `openEnvironment()`.
16283
16386
  *
@@ -16309,7 +16412,9 @@ var Granular = class _Granular {
16309
16412
  requestedOntology,
16310
16413
  sandboxId: environmentData.sandboxId,
16311
16414
  subjectId: environmentData.subjectId,
16312
- setupTriggerReason: options.reason || "new_environment"
16415
+ externalUserId: environmentData.subjectId,
16416
+ setupTriggerReason: options.reason || "new_environment",
16417
+ setupOperationKey: options.operationKey
16313
16418
  },
16314
16419
  environment
16315
16420
  );
@@ -16321,20 +16426,17 @@ var Granular = class _Granular {
16321
16426
  }
16322
16427
  return tag;
16323
16428
  }
16324
- buildManagedEnvironmentName(tag, versionId) {
16325
- return `__sdk__${tag}__${versionId}__pinned`;
16429
+ buildManagedEnvironmentName(tag, versionId, resetKey) {
16430
+ if (!resetKey) {
16431
+ return `__sdk__${tag}__${versionId}__tracked`;
16432
+ }
16433
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
16434
+ return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
16326
16435
  }
16327
16436
  isManagedEnvironmentName(environment, tagName) {
16328
16437
  const name = environment.environment || environment.envName || "";
16329
16438
  return name.startsWith(`__sdk__${tagName}__`);
16330
16439
  }
16331
- isPinnedToVersion(environment, versionId) {
16332
- return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
16333
- }
16334
- matchesTagTrackedEnvironment(environment, tagName, tagId) {
16335
- const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
16336
- return environment.tagId === tagId || environmentTagName === tagName || environment.environment === tagName || environment.envName === tagName || environment.environment === this.buildManagedEnvironmentName(tagName, environment.versionId) || environment.envName === this.buildManagedEnvironmentName(tagName, environment.versionId);
16337
- }
16338
16440
  sortEnvironmentsByRecency(environments) {
16339
16441
  return [...environments].sort(
16340
16442
  (left, right) => right.updatedAt - left.updatedAt
@@ -16384,48 +16486,119 @@ var Granular = class _Granular {
16384
16486
  `Tag "${tagName}" does not currently point to a build/version.`
16385
16487
  );
16386
16488
  }
16489
+ const slot = options.slot?.trim() || "default";
16490
+ const resetKey = options.resetKey?.trim() || void 0;
16491
+ const resolveActive = async (operationKey) => {
16492
+ const query = new URLSearchParams({ tagId: tag.tagId, slot });
16493
+ if (operationKey) query.set("operationKey", operationKey);
16494
+ try {
16495
+ const payload = await this.request(
16496
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
16497
+ );
16498
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
16499
+ } catch (error) {
16500
+ const message = error instanceof Error ? error.message : String(error);
16501
+ if (message.includes("404") || message.includes("not found")) {
16502
+ return null;
16503
+ }
16504
+ throw error;
16505
+ }
16506
+ };
16507
+ const activate = async (environment2) => {
16508
+ const payload = await this.request(
16509
+ "/control/environment-activations",
16510
+ {
16511
+ method: "POST",
16512
+ body: JSON.stringify({
16513
+ environmentId: environment2.environmentId,
16514
+ tagId: tag.tagId,
16515
+ slot,
16516
+ operationKey: resetKey,
16517
+ operation: resetKey ? "explicit_reset" : void 0
16518
+ })
16519
+ }
16520
+ );
16521
+ return normalizeEnvironmentData(payload.environment);
16522
+ };
16523
+ if (resetKey) {
16524
+ const resetEnvironment = await resolveActive(resetKey);
16525
+ if (resetEnvironment) {
16526
+ return {
16527
+ environment: resetEnvironment,
16528
+ requestedOntology: ontology,
16529
+ sandboxId: sandbox.sandboxId,
16530
+ subjectId: user.granularId,
16531
+ externalUserId: user.userId,
16532
+ // Retrying an explicit reset must also resume its durable setup run.
16533
+ // Otherwise a Container crash after queue submission would leave a
16534
+ // valid environment permanently marked as "running".
16535
+ setupTriggerReason: "explicit_reset",
16536
+ setupOperationKey: resetKey
16537
+ };
16538
+ }
16539
+ } else {
16540
+ const active = await resolveActive();
16541
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
16542
+ return {
16543
+ environment: active,
16544
+ requestedOntology: ontology,
16545
+ sandboxId: sandbox.sandboxId,
16546
+ subjectId: user.granularId,
16547
+ externalUserId: user.userId
16548
+ };
16549
+ }
16550
+ }
16387
16551
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
16388
16552
  const userEnvironments = allEnvironments.filter(
16389
- (environment) => environment.subjectId === user.granularId
16553
+ (environment2) => environment2.subjectId === user.granularId
16390
16554
  );
16391
16555
  const currentMatches = this.sortEnvironmentsByRecency(
16392
16556
  userEnvironments.filter(
16393
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
16557
+ (environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
16394
16558
  )
16395
16559
  );
16396
- if (currentMatches.length > 0) {
16560
+ if (!resetKey && currentMatches.length > 0) {
16561
+ const environment2 = await activate(currentMatches[0]);
16397
16562
  return {
16398
- environment: currentMatches[0],
16563
+ environment: environment2,
16399
16564
  requestedOntology: ontology,
16400
16565
  sandboxId: sandbox.sandboxId,
16401
- subjectId: user.granularId
16566
+ subjectId: user.granularId,
16567
+ externalUserId: user.userId
16402
16568
  };
16403
16569
  }
16404
16570
  const outdatedMatches = this.sortEnvironmentsByRecency(
16405
- userEnvironments.filter(
16406
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
16407
- )
16571
+ userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
16408
16572
  );
16409
16573
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
16574
+ const environment2 = await activate(outdatedMatches[0]);
16410
16575
  return {
16411
- environment: outdatedMatches[0],
16576
+ environment: environment2,
16412
16577
  requestedOntology: ontology,
16413
16578
  sandboxId: sandbox.sandboxId,
16414
- subjectId: user.granularId
16579
+ subjectId: user.granularId,
16580
+ externalUserId: user.userId
16415
16581
  };
16416
16582
  }
16583
+ const created = await this.environments.create(sandbox.sandboxId, {
16584
+ subjectId: user.granularId,
16585
+ environment: this.buildManagedEnvironmentName(
16586
+ tagName,
16587
+ targetVersionId,
16588
+ resetKey
16589
+ ),
16590
+ tagId: tag.tagId,
16591
+ permissionProfileId: null
16592
+ });
16593
+ const environment = await activate(created);
16417
16594
  return {
16418
- environment: await this.environments.create(sandbox.sandboxId, {
16419
- subjectId: user.granularId,
16420
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
16421
- tagId: tag.tagId,
16422
- versionId: targetVersionId,
16423
- permissionProfileId: null
16424
- }),
16595
+ environment,
16425
16596
  requestedOntology: ontology,
16426
16597
  sandboxId: sandbox.sandboxId,
16427
16598
  subjectId: user.granularId,
16428
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
16599
+ externalUserId: user.userId,
16600
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
16601
+ setupOperationKey: resetKey
16429
16602
  };
16430
16603
  }
16431
16604
  /**
@@ -16704,11 +16877,24 @@ var Granular = class _Granular {
16704
16877
  {
16705
16878
  method: "POST",
16706
16879
  body: JSON.stringify({
16707
- triggerReason: resolved.setupTriggerReason
16880
+ triggerReason: resolved.setupTriggerReason,
16881
+ operationKey: resolved.setupOperationKey
16708
16882
  })
16709
16883
  }
16710
16884
  );
16711
16885
  const setupRunId = setupRun.setupRunId;
16886
+ let claim = null;
16887
+ for (let attempt = 0; attempt < 3; attempt += 1) {
16888
+ claim = await this.request(
16889
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
16890
+ { method: "POST", body: JSON.stringify({}) }
16891
+ );
16892
+ if (claim.action !== "busy") break;
16893
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
16894
+ }
16895
+ if (!claim) {
16896
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
16897
+ }
16712
16898
  const updateSetupRun = async (patch) => {
16713
16899
  await this.request(
16714
16900
  `/control/environment-setup-runs/${setupRunId}`,
@@ -16718,10 +16904,26 @@ var Granular = class _Granular {
16718
16904
  }
16719
16905
  );
16720
16906
  };
16907
+ if (claim.action === "submitted") {
16908
+ const completedSetupRun = await this.request(
16909
+ `/control/environment-setup-runs/${setupRunId}`,
16910
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
16911
+ );
16912
+ const refreshedEnvironment = await this.environments.get(
16913
+ environment.environmentId
16914
+ );
16915
+ environment.syncEnvironmentData(refreshedEnvironment);
16916
+ return completedSetupRun;
16917
+ }
16918
+ if (claim.action === "busy" || claim.action === "terminal") {
16919
+ return claim.summary;
16920
+ }
16921
+ let importSequence = 0;
16721
16922
  const importerContext = {
16722
16923
  environmentId: environment.environmentId,
16723
16924
  sandboxId: environment.sandboxId,
16724
16925
  subjectId: environment.subjectId,
16926
+ externalUserId: resolved.externalUserId,
16725
16927
  reason: resolved.setupTriggerReason,
16726
16928
  incrementTotalObjectsToImportCount: async (n) => {
16727
16929
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -16738,7 +16940,10 @@ var Granular = class _Granular {
16738
16940
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
16739
16941
  batchSize: options?.batchSize,
16740
16942
  writeMode: options?.writeMode,
16741
- setupRunId
16943
+ setupRunId,
16944
+ // Sequence is deterministic for a retry of one importer hook. It
16945
+ // prevents a Container restart from creating a second queue import.
16946
+ operationKey: `${setupRunId}:import:${importSequence++}`
16742
16947
  })
16743
16948
  };
16744
16949
  try {