@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.
@@ -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,156 @@ 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
+ * Resolve one exact environment only when Granular confirms that it belongs
16335
+ * to the given external user in the requested sandbox (and, optionally,
16336
+ * tag). This is deliberately read-only: callers use it to keep an already
16337
+ * opened delegated workspace stable while a newer active environment is
16338
+ * being prepared in the background.
16339
+ */
16340
+ async getEnvironmentForUser(options) {
16341
+ const sandboxId = options.sandboxId.trim();
16342
+ const userId = options.userId.trim();
16343
+ const environmentId = options.environmentId.trim();
16344
+ const tagName = options.tag?.trim();
16345
+ if (!sandboxId || !userId || !environmentId) {
16346
+ throw new Error(
16347
+ "getEnvironmentForUser() requires sandboxId, userId, and environmentId."
16348
+ );
16349
+ }
16350
+ const subjects = await this.request(
16351
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16352
+ );
16353
+ const subject = (subjects.items || []).find(
16354
+ (item) => item.identityId === userId || item.userId === userId
16355
+ );
16356
+ const subjectId = subject?.subjectId || subject?.granularId;
16357
+ if (!subjectId) return null;
16358
+ let environment;
16359
+ try {
16360
+ environment = await this.environments.get(environmentId);
16361
+ } catch (error) {
16362
+ const message = error instanceof Error ? error.message : String(error);
16363
+ if (message.includes("404") || message.includes("not found")) {
16364
+ return null;
16365
+ }
16366
+ throw error;
16367
+ }
16368
+ if (environment.sandboxId !== sandboxId || environment.subjectId !== subjectId) {
16369
+ return null;
16370
+ }
16371
+ if (tagName) {
16372
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16373
+ const tag = (tags.items || []).find(
16374
+ (candidate) => candidate?.name === tagName
16375
+ );
16376
+ if (!tag || environment.tagId !== tag.tagId) {
16377
+ return null;
16378
+ }
16379
+ }
16380
+ return this.bindEnvironmentHandle(environment);
16381
+ }
16382
+ /**
16383
+ * Register one reviewed, pre-existing environment as the active workspace
16384
+ * for an external user. This is for a controlled migration only: it does
16385
+ * not create an environment and it does not run an importer.
16386
+ */
16387
+ async adoptEnvironmentForUser(options) {
16388
+ const sandboxId = options.sandboxId.trim();
16389
+ const tagName = options.tag.trim();
16390
+ const userId = options.userId.trim();
16391
+ const environmentId = options.environmentId.trim();
16392
+ if (!sandboxId || !tagName || !userId || !environmentId) {
16393
+ throw new Error(
16394
+ "adoptEnvironmentForUser() requires sandboxId, tag, userId, and environmentId."
16395
+ );
16396
+ }
16397
+ const subjects = await this.request(
16398
+ `/control/subjects?identityId=${encodeURIComponent(userId)}`
16399
+ );
16400
+ const subject = (subjects.items || []).find(
16401
+ (item) => item.identityId === userId || item.userId === userId
16402
+ );
16403
+ if (!subject?.subjectId && !subject?.granularId) {
16404
+ throw new Error(`No Granular subject exists for user ${userId}.`);
16405
+ }
16406
+ const tags = await this.request(`/control/sandboxes/${encodeURIComponent(sandboxId)}/tags`);
16407
+ const tag = (tags.items || []).find(
16408
+ (item) => item?.name === tagName
16409
+ );
16410
+ if (!tag) {
16411
+ throw new Error(`Tag ${tagName} was not found in sandbox ${sandboxId}.`);
16412
+ }
16413
+ const payload = await this.request(
16414
+ "/control/environment-activations/adopt",
16415
+ {
16416
+ method: "POST",
16417
+ body: JSON.stringify({
16418
+ environmentId,
16419
+ subjectId: subject.subjectId || subject.granularId,
16420
+ tagId: tag.tagId,
16421
+ slot: options.slot?.trim() || "default",
16422
+ confirmExistingData: true
16423
+ })
16424
+ }
16425
+ );
16426
+ if (!payload.environment) {
16427
+ throw new Error("Granular did not return the adopted environment.");
16428
+ }
16429
+ return this.bindEnvironmentHandle(
16430
+ normalizeEnvironmentData(payload.environment)
16431
+ );
16432
+ }
16281
16433
  /**
16282
16434
  * Deprecated compatibility alias for `openEnvironment()`.
16283
16435
  *
@@ -16309,7 +16461,9 @@ var Granular = class _Granular {
16309
16461
  requestedOntology,
16310
16462
  sandboxId: environmentData.sandboxId,
16311
16463
  subjectId: environmentData.subjectId,
16312
- setupTriggerReason: options.reason || "new_environment"
16464
+ externalUserId: environmentData.subjectId,
16465
+ setupTriggerReason: options.reason || "new_environment",
16466
+ setupOperationKey: options.operationKey
16313
16467
  },
16314
16468
  environment
16315
16469
  );
@@ -16321,20 +16475,17 @@ var Granular = class _Granular {
16321
16475
  }
16322
16476
  return tag;
16323
16477
  }
16324
- buildManagedEnvironmentName(tag, versionId) {
16325
- return `__sdk__${tag}__${versionId}__pinned`;
16478
+ buildManagedEnvironmentName(tag, versionId, resetKey) {
16479
+ if (!resetKey) {
16480
+ return `__sdk__${tag}__${versionId}__tracked`;
16481
+ }
16482
+ const safeResetKey = resetKey.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
16483
+ return `__sdk__${tag}__${versionId}__reset__${safeResetKey}`;
16326
16484
  }
16327
16485
  isManagedEnvironmentName(environment, tagName) {
16328
16486
  const name = environment.environment || environment.envName || "";
16329
16487
  return name.startsWith(`__sdk__${tagName}__`);
16330
16488
  }
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
16489
  sortEnvironmentsByRecency(environments) {
16339
16490
  return [...environments].sort(
16340
16491
  (left, right) => right.updatedAt - left.updatedAt
@@ -16384,48 +16535,119 @@ var Granular = class _Granular {
16384
16535
  `Tag "${tagName}" does not currently point to a build/version.`
16385
16536
  );
16386
16537
  }
16538
+ const slot = options.slot?.trim() || "default";
16539
+ const resetKey = options.resetKey?.trim() || void 0;
16540
+ const resolveActive = async (operationKey) => {
16541
+ const query = new URLSearchParams({ tagId: tag.tagId, slot });
16542
+ if (operationKey) query.set("operationKey", operationKey);
16543
+ try {
16544
+ const payload = await this.request(
16545
+ `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
16546
+ );
16547
+ return payload.environment ? normalizeEnvironmentData(payload.environment) : null;
16548
+ } catch (error) {
16549
+ const message = error instanceof Error ? error.message : String(error);
16550
+ if (message.includes("404") || message.includes("not found")) {
16551
+ return null;
16552
+ }
16553
+ throw error;
16554
+ }
16555
+ };
16556
+ const activate = async (environment2) => {
16557
+ const payload = await this.request(
16558
+ "/control/environment-activations",
16559
+ {
16560
+ method: "POST",
16561
+ body: JSON.stringify({
16562
+ environmentId: environment2.environmentId,
16563
+ tagId: tag.tagId,
16564
+ slot,
16565
+ operationKey: resetKey,
16566
+ operation: resetKey ? "explicit_reset" : void 0
16567
+ })
16568
+ }
16569
+ );
16570
+ return normalizeEnvironmentData(payload.environment);
16571
+ };
16572
+ if (resetKey) {
16573
+ const resetEnvironment = await resolveActive(resetKey);
16574
+ if (resetEnvironment) {
16575
+ return {
16576
+ environment: resetEnvironment,
16577
+ requestedOntology: ontology,
16578
+ sandboxId: sandbox.sandboxId,
16579
+ subjectId: user.granularId,
16580
+ externalUserId: user.userId,
16581
+ // Retrying an explicit reset must also resume its durable setup run.
16582
+ // Otherwise a Container crash after queue submission would leave a
16583
+ // valid environment permanently marked as "running".
16584
+ setupTriggerReason: "explicit_reset",
16585
+ setupOperationKey: resetKey
16586
+ };
16587
+ }
16588
+ } else {
16589
+ const active = await resolveActive();
16590
+ if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
16591
+ return {
16592
+ environment: active,
16593
+ requestedOntology: ontology,
16594
+ sandboxId: sandbox.sandboxId,
16595
+ subjectId: user.granularId,
16596
+ externalUserId: user.userId
16597
+ };
16598
+ }
16599
+ }
16387
16600
  const allEnvironments = await this.environments.list(sandbox.sandboxId);
16388
16601
  const userEnvironments = allEnvironments.filter(
16389
- (environment) => environment.subjectId === user.granularId
16602
+ (environment2) => environment2.subjectId === user.granularId
16390
16603
  );
16391
16604
  const currentMatches = this.sortEnvironmentsByRecency(
16392
16605
  userEnvironments.filter(
16393
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
16606
+ (environment2) => environment2.tagId === tag.tagId && environment2.versionId === targetVersionId
16394
16607
  )
16395
16608
  );
16396
- if (currentMatches.length > 0) {
16609
+ if (!resetKey && currentMatches.length > 0) {
16610
+ const environment2 = await activate(currentMatches[0]);
16397
16611
  return {
16398
- environment: currentMatches[0],
16612
+ environment: environment2,
16399
16613
  requestedOntology: ontology,
16400
16614
  sandboxId: sandbox.sandboxId,
16401
- subjectId: user.granularId
16615
+ subjectId: user.granularId,
16616
+ externalUserId: user.userId
16402
16617
  };
16403
16618
  }
16404
16619
  const outdatedMatches = this.sortEnvironmentsByRecency(
16405
- userEnvironments.filter(
16406
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
16407
- )
16620
+ userEnvironments.filter((environment2) => environment2.tagId === tag.tagId)
16408
16621
  );
16409
16622
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
16623
+ const environment2 = await activate(outdatedMatches[0]);
16410
16624
  return {
16411
- environment: outdatedMatches[0],
16625
+ environment: environment2,
16412
16626
  requestedOntology: ontology,
16413
16627
  sandboxId: sandbox.sandboxId,
16414
- subjectId: user.granularId
16628
+ subjectId: user.granularId,
16629
+ externalUserId: user.userId
16415
16630
  };
16416
16631
  }
16632
+ const created = await this.environments.create(sandbox.sandboxId, {
16633
+ subjectId: user.granularId,
16634
+ environment: this.buildManagedEnvironmentName(
16635
+ tagName,
16636
+ targetVersionId,
16637
+ resetKey
16638
+ ),
16639
+ tagId: tag.tagId,
16640
+ permissionProfileId: null
16641
+ });
16642
+ const environment = await activate(created);
16417
16643
  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
- }),
16644
+ environment,
16425
16645
  requestedOntology: ontology,
16426
16646
  sandboxId: sandbox.sandboxId,
16427
16647
  subjectId: user.granularId,
16428
- setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
16648
+ externalUserId: user.userId,
16649
+ setupTriggerReason: resetKey ? "explicit_reset" : outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment",
16650
+ setupOperationKey: resetKey
16429
16651
  };
16430
16652
  }
16431
16653
  /**
@@ -16704,11 +16926,24 @@ var Granular = class _Granular {
16704
16926
  {
16705
16927
  method: "POST",
16706
16928
  body: JSON.stringify({
16707
- triggerReason: resolved.setupTriggerReason
16929
+ triggerReason: resolved.setupTriggerReason,
16930
+ operationKey: resolved.setupOperationKey
16708
16931
  })
16709
16932
  }
16710
16933
  );
16711
16934
  const setupRunId = setupRun.setupRunId;
16935
+ let claim = null;
16936
+ for (let attempt = 0; attempt < 3; attempt += 1) {
16937
+ claim = await this.request(
16938
+ `/control/environment-setup-runs/${setupRunId}/importer-claim`,
16939
+ { method: "POST", body: JSON.stringify({}) }
16940
+ );
16941
+ if (claim.action !== "busy") break;
16942
+ await sleep(Math.min(3e4, Math.max(250, claim.retryAfterMs || 1e3)));
16943
+ }
16944
+ if (!claim) {
16945
+ throw new Error(`Unable to claim environment setup run ${setupRunId}.`);
16946
+ }
16712
16947
  const updateSetupRun = async (patch) => {
16713
16948
  await this.request(
16714
16949
  `/control/environment-setup-runs/${setupRunId}`,
@@ -16718,10 +16953,26 @@ var Granular = class _Granular {
16718
16953
  }
16719
16954
  );
16720
16955
  };
16956
+ if (claim.action === "submitted") {
16957
+ const completedSetupRun = await this.request(
16958
+ `/control/environment-setup-runs/${setupRunId}`,
16959
+ { method: "PATCH", body: JSON.stringify({ markHookCompleted: true }) }
16960
+ );
16961
+ const refreshedEnvironment = await this.environments.get(
16962
+ environment.environmentId
16963
+ );
16964
+ environment.syncEnvironmentData(refreshedEnvironment);
16965
+ return completedSetupRun;
16966
+ }
16967
+ if (claim.action === "busy" || claim.action === "terminal") {
16968
+ return claim.summary;
16969
+ }
16970
+ let importSequence = 0;
16721
16971
  const importerContext = {
16722
16972
  environmentId: environment.environmentId,
16723
16973
  sandboxId: environment.sandboxId,
16724
16974
  subjectId: environment.subjectId,
16975
+ externalUserId: resolved.externalUserId,
16725
16976
  reason: resolved.setupTriggerReason,
16726
16977
  incrementTotalObjectsToImportCount: async (n) => {
16727
16978
  const safeIncrement = Math.max(0, Math.trunc(n));
@@ -16738,7 +16989,10 @@ var Granular = class _Granular {
16738
16989
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
16739
16990
  batchSize: options?.batchSize,
16740
16991
  writeMode: options?.writeMode,
16741
- setupRunId
16992
+ setupRunId,
16993
+ // Sequence is deterministic for a retry of one importer hook. It
16994
+ // prevents a Container restart from creating a second queue import.
16995
+ operationKey: `${setupRunId}:import:${importSequence++}`
16742
16996
  })
16743
16997
  };
16744
16998
  try {