@granular-software/sdk 0.4.51 → 0.4.53

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,156 @@ 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
+ * Resolve one exact environment only when Granular confirms that it belongs
16430
+ * to the given external user in the requested sandbox (and, optionally,
16431
+ * tag). This is deliberately read-only: callers use it to keep an already
16432
+ * opened delegated workspace stable while a newer active environment is
16433
+ * being prepared in the background.
16434
+ */
16435
+ async getEnvironmentForUser(options) {
16436
+ const sandboxId = options.sandboxId.trim();
16437
+ const userId = options.userId.trim();
16438
+ const environmentId = options.environmentId.trim();
16439
+ const tagName = options.tag?.trim();
16440
+ if (!sandboxId || !userId || !environmentId) {
16441
+ throw new Error(
16442
+ "getEnvironmentForUser() requires sandboxId, userId, and environmentId."
16443
+ );
16444
+ }
16445
+ const subjects = await this.request(
16446
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16447
+ );
16448
+ const subject = (subjects.items || []).find(
16449
+ (item) => item.identityId === userId || item.userId === userId
16450
+ );
16451
+ const subjectId = subject?.subjectId || subject?.granularId;
16452
+ if (!subjectId) return null;
16453
+ let environment;
16454
+ try {
16455
+ environment = await this.environments.get(environmentId);
16456
+ } catch (error) {
16457
+ const message = error instanceof Error ? error.message : String(error);
16458
+ if (message.includes("404") || message.includes("not found")) {
16459
+ return null;
16460
+ }
16461
+ throw error;
16462
+ }
16463
+ if (environment.sandboxId !== sandboxId || environment.subjectId !== subjectId) {
16464
+ return null;
16465
+ }
16466
+ if (tagName) {
16467
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16468
+ const tag = (tags.items || []).find(
16469
+ (candidate) => candidate?.name === tagName
16470
+ );
16471
+ if (!tag || environment.tagId !== tag.tagId) {
16472
+ return null;
16473
+ }
16474
+ }
16475
+ return this.bindEnvironmentHandle(environment);
16476
+ }
16477
+ /**
16478
+ * Register one reviewed, pre-existing environment as the active workspace
16479
+ * for an external user. This is for a controlled migration only: it does
16480
+ * not create an environment and it does not run an importer.
16481
+ */
16482
+ async adoptEnvironmentForUser(options) {
16483
+ const sandboxId = options.sandboxId.trim();
16484
+ const tagName = options.tag.trim();
16485
+ const userId = options.userId.trim();
16486
+ const environmentId = options.environmentId.trim();
16487
+ if (!sandboxId || !tagName || !userId || !environmentId) {
16488
+ throw new Error(
16489
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
16490
+ );
16491
+ }
16492
+ const subjects = await this.request(
16493
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16494
+ );
16495
+ const subject = (subjects.items || []).find(
16496
+ (item) => item.identityId === userId || item.userId === userId
16497
+ );
16498
+ if (!subject?.subjectId && !subject?.granularId) {
16499
+ throw new Error(`No Granular subject exists for user ${userId}.`);
16500
+ }
16501
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16502
+ const tag = (tags.items || []).find(
16503
+ (item) => item?.name === tagName
16504
+ );
16505
+ if (!tag) {
16506
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
16507
+ }
16508
+ const payload = await this.request(
16509
+ "/control/environment-activations/adopt",
16510
+ {
16511
+ method: "POST",
16512
+ body: JSON.stringify({
16513
+ environmentId,
16514
+ subjectId: subject.subjectId || subject.granularId,
16515
+ tagId: tag.tagId,
16516
+ slot: options.slot?.trim() || "default",
16517
+ confirmExistingData: true
16518
+ })
16519
+ }
16520
+ );
16521
+ if (!payload.environment) {
16522
+ throw new Error("Granular did not return the adopted environment.");
16523
+ }
16524
+ return this.bindEnvironmentHandle(
16525
+ normalizeEnvironmentData(payload.environment)
16526
+ );
16527
+ }
16376
16528
  /**
16377
16529
  * Deprecated compatibility alias for `openEnvironment()`.
16378
16530
  *
@@ -16404,7 +16556,9 @@ var Granular = class _Granular {
16404
16556
  requestedOntology,
16405
16557
  sandboxId: environmentData.sandboxId,
16406
16558
  subjectId: environmentData.subjectId,
16407
- setupTriggerReason: options.reason || "new_environment"
16559
+ externalUserId: environmentData.subjectId,
16560
+ setupTriggerReason: options.reason || "new_environment",
16561
+ setupOperationKey: options.operationKey
16408
16562
  },
16409
16563
  environment
16410
16564
  );
@@ -16416,20 +16570,17 @@ var Granular = class _Granular {
16416
16570
  }
16417
16571
  return tag;
16418
16572
  }
16419
- buildManagedEnvironmentName(tag, versionId) {
16420
- return `__sdk__${tag}__${versionId}__pinned`;
16573
+ buildManagedEnvironmentName(tag, versionId, resetKey) {
16574
+ if (!resetKey) {
16575
+ return `__sdk__${tag}__${versionId}__tracked`;
16576
+ }
16577
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
16578
+ return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
16421
16579
  }
16422
16580
  isManagedEnvironmentName(environment, tagName) {
16423
16581
  const name = environment.environment || environment.envName || "";
16424
16582
  return name.startsWith(`__sdk__${tagName}__`);
16425
16583
  }
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
16584
  sortEnvironmentsByRecency(environments) {
16434
16585
  return [...environments].sort(
16435
16586
  (left, right) => right.updatedAt - left.updatedAt
@@ -16479,48 +16630,119 @@ var Granular = class _Granular {
16479
16630
  `Tag "${tagName}" does not currently point to a build/version.`
16480
16631
  );
16481
16632
  }
16633
+ const slot = options.slot?.trim() || "default";
16634
+ const resetKey = options.resetKey?.trim() || void 0;
16635
+ const resolveActive = async (operationKey) => {
16636
+ const query = new URLSearchParams({ tagId: tag.tagId, slot });
16637
+ if (operationKey) query.set("operationKey", operationKey);
16638
+ try {
16639
+ const payload = await this.request(
16640
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
16641
+ );
16642
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
16643
+ } catch (error) {
16644
+ const message = error instanceof Error ? error.message : String(error);
16645
+ if (message.includes("404") || message.includes("not found")) {
16646
+ return null;
16647
+ }
16648
+ throw error;
16649
+ }
16650
+ };
16651
+ const activate = async (environment2) => {
16652
+ const payload = await this.request(
16653
+ "/control/environment-activations",
16654
+ {
16655
+ method: "POST",
16656
+ body: JSON.stringify({
16657
+ environmentId: environment2.environmentId,
16658
+ tagId: tag.tagId,
16659
+ slot,
16660
+ operationKey: resetKey,
16661
+ operation: resetKey ? "explicit_reset" : void 0
16662
+ })
16663
+ }
16664
+ );
16665
+ return normalizeEnvironmentData(payload.environment);
16666
+ };
16667
+ if (resetKey) {
16668
+ const resetEnvironment = await resolveActive(resetKey);
16669
+ if (resetEnvironment) {
16670
+ return {
16671
+ environment: resetEnvironment,
16672
+ requestedOntology: ontology,
16673
+ sandboxId: sandbox.sandboxId,
16674
+ subjectId: user.granularId,
16675
+ externalUserId: user.userId,
16676
+ // Retrying an explicit reset must also resume its durable setup run.
16677
+ // Otherwise a Container crash after queue submission would leave a
16678
+ // valid environment permanently marked as "running".
16679
+ setupTriggerReason: "explicit_reset",
16680
+ setupOperationKey: resetKey
16681
+ };
16682
+ }
16683
+ } else {
16684
+ const active = await resolveActive();
16685
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
16686
+ return {
16687
+ environment: active,
16688
+ requestedOntology: ontology,
16689
+ sandboxId: sandbox.sandboxId,
16690
+ subjectId: user.granularId,
16691
+ externalUserId: user.userId
16692
+ };
16693
+ }
16694
+ }
16482
16695
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
16483
16696
  const userEnvironments = allEnvironments.filter(
16484
- (environment) => environment.subjectId === user.granularId
16697
+ (environment2) => environment2.subjectId === user.granularId
16485
16698
  );
16486
16699
  const currentMatches = this.sortEnvironmentsByRecency(
16487
16700
  userEnvironments.filter(
16488
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
16701
+ (environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
16489
16702
  )
16490
16703
  );
16491
- if (currentMatches.length > 0) {
16704
+ if (!resetKey && currentMatches.length > 0) {
16705
+ const environment2 = await activate(currentMatches[0]);
16492
16706
  return {
16493
- environment: currentMatches[0],
16707
+ environment: environment2,
16494
16708
  requestedOntology: ontology,
16495
16709
  sandboxId: sandbox.sandboxId,
16496
- subjectId: user.granularId
16710
+ subjectId: user.granularId,
16711
+ externalUserId: user.userId
16497
16712
  };
16498
16713
  }
16499
16714
  const outdatedMatches = this.sortEnvironmentsByRecency(
16500
- userEnvironments.filter(
16501
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
16502
- )
16715
+ userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
16503
16716
  );
16504
16717
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
16718
+ const environment2 = await activate(outdatedMatches[0]);
16505
16719
  return {
16506
- environment: outdatedMatches[0],
16720
+ environment: environment2,
16507
16721
  requestedOntology: ontology,
16508
16722
  sandboxId: sandbox.sandboxId,
16509
- subjectId: user.granularId
16723
+ subjectId: user.granularId,
16724
+ externalUserId: user.userId
16510
16725
  };
16511
16726
  }
16727
+ const created = await this.environments.create(sandbox.sandboxId, {
16728
+ subjectId: user.granularId,
16729
+ environment: this.buildManagedEnvironmentName(
16730
+ tagName,
16731
+ targetVersionId,
16732
+ resetKey
16733
+ ),
16734
+ tagId: tag.tagId,
16735
+ permissionProfileId: null
16736
+ });
16737
+ const environment = await activate(created);
16512
16738
  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
- }),
16739
+ environment,
16520
16740
  requestedOntology: ontology,
16521
16741
  sandboxId: sandbox.sandboxId,
16522
16742
  subjectId: user.granularId,
16523
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
16743
+ externalUserId: user.userId,
16744
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
16745
+ setupOperationKey: resetKey
16524
16746
  };
16525
16747
  }
16526
16748
  /**
@@ -16799,11 +17021,24 @@ var Granular = class _Granular {
16799
17021
  {
16800
17022
  method: "POST",
16801
17023
  body: JSON.stringify({
16802
- triggerReason: resolved.setupTriggerReason
17024
+ triggerReason: resolved.setupTriggerReason,
17025
+ operationKey: resolved.setupOperationKey
16803
17026
  })
16804
17027
  }
16805
17028
  );
16806
17029
  const setupRunId = setupRun.setupRunId;
17030
+ let claim = null;
17031
+ for (let attempt = 0; attempt < 3; attempt += 1) {
17032
+ claim = await this.request(
17033
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
17034
+ { method: "POST", body: JSON.stringify({}) }
17035
+ );
17036
+ if (claim.action !== "busy") break;
17037
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
17038
+ }
17039
+ if (!claim) {
17040
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
17041
+ }
16807
17042
  const updateSetupRun = async (patch) => {
16808
17043
  await this.request(
16809
17044
  `/control/environment-setup-runs/${setupRunId}`,
@@ -16813,10 +17048,26 @@ var Granular = class _Granular {
16813
17048
  }
16814
17049
  );
16815
17050
  };
17051
+ if (claim.action === "submitted") {
17052
+ const completedSetupRun = await this.request(
17053
+ `/control/environment-setup-runs/${setupRunId}`,
17054
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
17055
+ );
17056
+ const refreshedEnvironment = await this.environments.get(
17057
+ environment.environmentId
17058
+ );
17059
+ environment.syncEnvironmentData(refreshedEnvironment);
17060
+ return completedSetupRun;
17061
+ }
17062
+ if (claim.action === "busy" || claim.action === "terminal") {
17063
+ return claim.summary;
17064
+ }
17065
+ let importSequence = 0;
16816
17066
  const importerContext = {
16817
17067
  environmentId: environment.environmentId,
16818
17068
  sandboxId: environment.sandboxId,
16819
17069
  subjectId: environment.subjectId,
17070
+ externalUserId: resolved.externalUserId,
16820
17071
  reason: resolved.setupTriggerReason,
16821
17072
  incrementTotalObjectsToImportCount: async (n) => {
16822
17073
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -16833,7 +17084,10 @@ var Granular = class _Granular {
16833
17084
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
16834
17085
  batchSize: options?.batchSize,
16835
17086
  writeMode: options?.writeMode,
16836
- setupRunId
17087
+ setupRunId,
17088
+ // Sequence is deterministic for a retry of one importer hook. It
17089
+ // prevents a Container restart from creating a second queue import.
17090
+ operationKey: `${setupRunId}:import:${importSequence++}`
16837
17091
  })
16838
17092
  };
16839
17093
  try {