@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.
package/dist/index.mjs CHANGED
@@ -14287,7 +14287,8 @@ function normalizeEnvironmentSetupSummary(setup) {
14287
14287
  environmentId: String(setup.environmentId || ""),
14288
14288
  sandboxId: String(setup.sandboxId || ""),
14289
14289
  subjectId: String(setup.subjectId || ""),
14290
- triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
14290
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : setup.triggerReason === "explicit_reset" ? "explicit_reset" : "new_environment",
14291
+ operationKey: typeof setup.operationKey === "string" ? setup.operationKey : null,
14291
14292
  lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
14292
14293
  stage: typeof setup.stage === "string" ? setup.stage : null,
14293
14294
  totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
@@ -15569,6 +15570,7 @@ var Environment = class _Environment {
15569
15570
  records: recordsToImport,
15570
15571
  batchSize: options.batchSize,
15571
15572
  setupRunId: options.setupRunId,
15573
+ operationKey: options.operationKey,
15572
15574
  writeMode: options.writeMode
15573
15575
  })
15574
15576
  }
@@ -16373,6 +16375,107 @@ var Granular = class _Granular {
16373
16375
  await this.maybeRunEnvironmentImporter(resolved, environment);
16374
16376
  return environment;
16375
16377
  }
16378
+ /**
16379
+ * Read the active environment selected by Granular for an already-recorded
16380
+ * external user. This is intentionally read-only: browser/login code must
16381
+ * not create subjects or environments as a side effect.
16382
+ */
16383
+ async getActiveEnvironmentForUser(options) {
16384
+ const sandboxId = options.sandboxId.trim();
16385
+ const tagName = options.tag.trim();
16386
+ const userId = options.userId.trim();
16387
+ if (!sandboxId || !tagName || !userId) {
16388
+ throw new Error(
16389
+ "getActiveEnvironmentForUser() requires sandboxId, tag, and userId."
16390
+ );
16391
+ }
16392
+ const subjects = await this.request(
16393
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16394
+ );
16395
+ const subject = (subjects.items || []).find(
16396
+ (item) => item.identityId === userId || item.userId === userId
16397
+ );
16398
+ if (!subject?.subjectId && !subject?.granularId) {
16399
+ return null;
16400
+ }
16401
+ const subjectId = subject.subjectId || subject.granularId;
16402
+ const tags = await this.request(
16403
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`
16404
+ );
16405
+ const tag = (tags.items || []).find(
16406
+ (item) => item?.name === tagName
16407
+ );
16408
+ if (!tag) return null;
16409
+ const query = new URLSearchParams({
16410
+ tagId: tag.tagId,
16411
+ slot: options.slot?.trim() || "default"
16412
+ });
16413
+ try {
16414
+ const payload = await this.request(
16415
+ `/control/sandboxes/${encodeURIComponent(sandboxId)}/subjects/${encodeURIComponent(subjectId)}/active-environment?${query.toString()}`
16416
+ );
16417
+ return payload.environment ? this.bindEnvironmentHandle(
16418
+ normalizeEnvironmentData(payload.environment)
16419
+ ) : null;
16420
+ } catch (error) {
16421
+ const message = error instanceof Error ? error.message : String(error);
16422
+ if (message.includes("404") || message.includes("not found")) {
16423
+ return null;
16424
+ }
16425
+ throw error;
16426
+ }
16427
+ }
16428
+ /**
16429
+ * Register one reviewed, pre-existing environment as the active workspace
16430
+ * for an external user. This is for a controlled migration only: it does
16431
+ * not create an environment and it does not run an importer.
16432
+ */
16433
+ async adoptEnvironmentForUser(options) {
16434
+ const sandboxId = options.sandboxId.trim();
16435
+ const tagName = options.tag.trim();
16436
+ const userId = options.userId.trim();
16437
+ const environmentId = options.environmentId.trim();
16438
+ if (!sandboxId || !tagName || !userId || !environmentId) {
16439
+ throw new Error(
16440
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
16441
+ );
16442
+ }
16443
+ const subjects = await this.request(
16444
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16445
+ );
16446
+ const subject = (subjects.items || []).find(
16447
+ (item) => item.identityId === userId || item.userId === userId
16448
+ );
16449
+ if (!subject?.subjectId && !subject?.granularId) {
16450
+ throw new Error(`No Granular subject exists for user ${userId}.`);
16451
+ }
16452
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16453
+ const tag = (tags.items || []).find(
16454
+ (item) => item?.name === tagName
16455
+ );
16456
+ if (!tag) {
16457
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
16458
+ }
16459
+ const payload = await this.request(
16460
+ "/control/environment-activations/adopt",
16461
+ {
16462
+ method: "POST",
16463
+ body: JSON.stringify({
16464
+ environmentId,
16465
+ subjectId: subject.subjectId || subject.granularId,
16466
+ tagId: tag.tagId,
16467
+ slot: options.slot?.trim() || "default",
16468
+ confirmExistingData: true
16469
+ })
16470
+ }
16471
+ );
16472
+ if (!payload.environment) {
16473
+ throw new Error("Granular did not return the adopted environment.");
16474
+ }
16475
+ return this.bindEnvironmentHandle(
16476
+ normalizeEnvironmentData(payload.environment)
16477
+ );
16478
+ }
16376
16479
  /**
16377
16480
  * Deprecated compatibility alias for `openEnvironment()`.
16378
16481
  *
@@ -16404,7 +16507,9 @@ var Granular = class _Granular {
16404
16507
  requestedOntology,
16405
16508
  sandboxId: environmentData.sandboxId,
16406
16509
  subjectId: environmentData.subjectId,
16407
- setupTriggerReason: options.reason || "new_environment"
16510
+ externalUserId: environmentData.subjectId,
16511
+ setupTriggerReason: options.reason || "new_environment",
16512
+ setupOperationKey: options.operationKey
16408
16513
  },
16409
16514
  environment
16410
16515
  );
@@ -16416,20 +16521,17 @@ var Granular = class _Granular {
16416
16521
  }
16417
16522
  return tag;
16418
16523
  }
16419
- buildManagedEnvironmentName(tag, versionId) {
16420
- return `__sdk__${tag}__${versionId}__pinned`;
16524
+ buildManagedEnvironmentName(tag, versionId, resetKey) {
16525
+ if (!resetKey) {
16526
+ return `__sdk__${tag}__${versionId}__tracked`;
16527
+ }
16528
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
16529
+ return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
16421
16530
  }
16422
16531
  isManagedEnvironmentName(environment, tagName) {
16423
16532
  const name = environment.environment || environment.envName || "";
16424
16533
  return name.startsWith(`__sdk__${tagName}__`);
16425
16534
  }
16426
- isPinnedToVersion(environment, versionId) {
16427
- return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
16428
- }
16429
- matchesTagTrackedEnvironment(environment, tagName, tagId) {
16430
- const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
16431
- 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);
16432
- }
16433
16535
  sortEnvironmentsByRecency(environments) {
16434
16536
  return [...environments].sort(
16435
16537
  (left, right) => right.updatedAt - left.updatedAt
@@ -16479,48 +16581,119 @@ var Granular = class _Granular {
16479
16581
  `Tag "${tagName}" does not currently point to a build/version.`
16480
16582
  );
16481
16583
  }
16584
+ const slot = options.slot?.trim() || "default";
16585
+ const resetKey = options.resetKey?.trim() || void 0;
16586
+ const resolveActive = async (operationKey) => {
16587
+ const query = new URLSearchParams({ tagId: tag.tagId, slot });
16588
+ if (operationKey) query.set("operationKey", operationKey);
16589
+ try {
16590
+ const payload = await this.request(
16591
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
16592
+ );
16593
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
16594
+ } catch (error) {
16595
+ const message = error instanceof Error ? error.message : String(error);
16596
+ if (message.includes("404") || message.includes("not found")) {
16597
+ return null;
16598
+ }
16599
+ throw error;
16600
+ }
16601
+ };
16602
+ const activate = async (environment2) => {
16603
+ const payload = await this.request(
16604
+ "/control/environment-activations",
16605
+ {
16606
+ method: "POST",
16607
+ body: JSON.stringify({
16608
+ environmentId: environment2.environmentId,
16609
+ tagId: tag.tagId,
16610
+ slot,
16611
+ operationKey: resetKey,
16612
+ operation: resetKey ? "explicit_reset" : void 0
16613
+ })
16614
+ }
16615
+ );
16616
+ return normalizeEnvironmentData(payload.environment);
16617
+ };
16618
+ if (resetKey) {
16619
+ const resetEnvironment = await resolveActive(resetKey);
16620
+ if (resetEnvironment) {
16621
+ return {
16622
+ environment: resetEnvironment,
16623
+ requestedOntology: ontology,
16624
+ sandboxId: sandbox.sandboxId,
16625
+ subjectId: user.granularId,
16626
+ externalUserId: user.userId,
16627
+ // Retrying an explicit reset must also resume its durable setup run.
16628
+ // Otherwise a Container crash after queue submission would leave a
16629
+ // valid environment permanently marked as "running".
16630
+ setupTriggerReason: "explicit_reset",
16631
+ setupOperationKey: resetKey
16632
+ };
16633
+ }
16634
+ } else {
16635
+ const active = await resolveActive();
16636
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
16637
+ return {
16638
+ environment: active,
16639
+ requestedOntology: ontology,
16640
+ sandboxId: sandbox.sandboxId,
16641
+ subjectId: user.granularId,
16642
+ externalUserId: user.userId
16643
+ };
16644
+ }
16645
+ }
16482
16646
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
16483
16647
  const userEnvironments = allEnvironments.filter(
16484
- (environment) => environment.subjectId === user.granularId
16648
+ (environment2) => environment2.subjectId === user.granularId
16485
16649
  );
16486
16650
  const currentMatches = this.sortEnvironmentsByRecency(
16487
16651
  userEnvironments.filter(
16488
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
16652
+ (environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
16489
16653
  )
16490
16654
  );
16491
- if (currentMatches.length > 0) {
16655
+ if (!resetKey && currentMatches.length > 0) {
16656
+ const environment2 = await activate(currentMatches[0]);
16492
16657
  return {
16493
- environment: currentMatches[0],
16658
+ environment: environment2,
16494
16659
  requestedOntology: ontology,
16495
16660
  sandboxId: sandbox.sandboxId,
16496
- subjectId: user.granularId
16661
+ subjectId: user.granularId,
16662
+ externalUserId: user.userId
16497
16663
  };
16498
16664
  }
16499
16665
  const outdatedMatches = this.sortEnvironmentsByRecency(
16500
- userEnvironments.filter(
16501
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
16502
- )
16666
+ userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
16503
16667
  );
16504
16668
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
16669
+ const environment2 = await activate(outdatedMatches[0]);
16505
16670
  return {
16506
- environment: outdatedMatches[0],
16671
+ environment: environment2,
16507
16672
  requestedOntology: ontology,
16508
16673
  sandboxId: sandbox.sandboxId,
16509
- subjectId: user.granularId
16674
+ subjectId: user.granularId,
16675
+ externalUserId: user.userId
16510
16676
  };
16511
16677
  }
16678
+ const created = await this.environments.create(sandbox.sandboxId, {
16679
+ subjectId: user.granularId,
16680
+ environment: this.buildManagedEnvironmentName(
16681
+ tagName,
16682
+ targetVersionId,
16683
+ resetKey
16684
+ ),
16685
+ tagId: tag.tagId,
16686
+ permissionProfileId: null
16687
+ });
16688
+ const environment = await activate(created);
16512
16689
  return {
16513
- environment: await this.environments.create(sandbox.sandboxId, {
16514
- subjectId: user.granularId,
16515
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
16516
- tagId: tag.tagId,
16517
- versionId: targetVersionId,
16518
- permissionProfileId: null
16519
- }),
16690
+ environment,
16520
16691
  requestedOntology: ontology,
16521
16692
  sandboxId: sandbox.sandboxId,
16522
16693
  subjectId: user.granularId,
16523
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
16694
+ externalUserId: user.userId,
16695
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
16696
+ setupOperationKey: resetKey
16524
16697
  };
16525
16698
  }
16526
16699
  /**
@@ -16799,11 +16972,24 @@ var Granular = class _Granular {
16799
16972
  {
16800
16973
  method: "POST",
16801
16974
  body: JSON.stringify({
16802
- triggerReason: resolved.setupTriggerReason
16975
+ triggerReason: resolved.setupTriggerReason,
16976
+ operationKey: resolved.setupOperationKey
16803
16977
  })
16804
16978
  }
16805
16979
  );
16806
16980
  const setupRunId = setupRun.setupRunId;
16981
+ let claim = null;
16982
+ for (let attempt = 0; attempt < 3; attempt += 1) {
16983
+ claim = await this.request(
16984
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
16985
+ { method: "POST", body: JSON.stringify({}) }
16986
+ );
16987
+ if (claim.action !== "busy") break;
16988
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
16989
+ }
16990
+ if (!claim) {
16991
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
16992
+ }
16807
16993
  const updateSetupRun = async (patch) => {
16808
16994
  await this.request(
16809
16995
  `/control/environment-setup-runs/${setupRunId}`,
@@ -16813,10 +16999,26 @@ var Granular = class _Granular {
16813
16999
  }
16814
17000
  );
16815
17001
  };
17002
+ if (claim.action === "submitted") {
17003
+ const completedSetupRun = await this.request(
17004
+ `/control/environment-setup-runs/${setupRunId}`,
17005
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
17006
+ );
17007
+ const refreshedEnvironment = await this.environments.get(
17008
+ environment.environmentId
17009
+ );
17010
+ environment.syncEnvironmentData(refreshedEnvironment);
17011
+ return completedSetupRun;
17012
+ }
17013
+ if (claim.action === "busy" || claim.action === "terminal") {
17014
+ return claim.summary;
17015
+ }
17016
+ let importSequence = 0;
16816
17017
  const importerContext = {
16817
17018
  environmentId: environment.environmentId,
16818
17019
  sandboxId: environment.sandboxId,
16819
17020
  subjectId: environment.subjectId,
17021
+ externalUserId: resolved.externalUserId,
16820
17022
  reason: resolved.setupTriggerReason,
16821
17023
  incrementTotalObjectsToImportCount: async (n) => {
16822
17024
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -16833,7 +17035,10 @@ var Granular = class _Granular {
16833
17035
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
16834
17036
  batchSize: options?.batchSize,
16835
17037
  writeMode: options?.writeMode,
16836
- setupRunId
17038
+ setupRunId,
17039
+ // Sequence is deterministic for a retry of one importer hook. It
17040
+ // prevents a Container restart from creating a second queue import.
17041
+ operationKey: `${setupRunId}:import:${importSequence++}`
16837
17042
  })
16838
17043
  };
16839
17044
  try {