@tailor-platform/sdk 1.47.1 → 1.49.0

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/{actor-jk4-f0yp.d.mts → actor-BeIEiPYM.d.mts} +2 -2
  3. package/dist/{application-C7H7y0hS.mjs → application-CZMzt9jL.mjs} +82 -18
  4. package/dist/application-CZMzt9jL.mjs.map +1 -0
  5. package/dist/application-v_E2W-Fz.mjs +4 -0
  6. package/dist/brand-D-d15jx3.mjs.map +1 -1
  7. package/dist/cli/index.mjs +55 -31
  8. package/dist/cli/index.mjs.map +1 -1
  9. package/dist/cli/lib.d.mts +6 -6
  10. package/dist/cli/lib.mjs +6 -6
  11. package/dist/cli/lib.mjs.map +1 -1
  12. package/dist/cli/skills.mjs.map +1 -1
  13. package/dist/{client-DCqdtFte.mjs → client-CPW1N1Rs.mjs} +1 -1
  14. package/dist/{client-DbyKSN1F.mjs → client-_kHh0Pip.mjs} +2 -2
  15. package/dist/{client-DbyKSN1F.mjs.map → client-_kHh0Pip.mjs.map} +1 -1
  16. package/dist/configure/index.d.mts +4 -4
  17. package/dist/configure/index.mjs +51 -3
  18. package/dist/configure/index.mjs.map +1 -1
  19. package/dist/{crashreport-CNSw_BrJ.mjs → crashreport-CvmdFs4i.mjs} +5 -5
  20. package/dist/crashreport-CvmdFs4i.mjs.map +1 -0
  21. package/dist/{crashreport-DXGFd16F.mjs → crashreport-DHJuSmUc.mjs} +1 -1
  22. package/dist/enum-constants-C3KSpsYj.mjs.map +1 -1
  23. package/dist/{errors-wNQxQQBH.mjs → errors-pMPXghkO.mjs} +1 -1
  24. package/dist/{errors-wNQxQQBH.mjs.map → errors-pMPXghkO.mjs.map} +1 -1
  25. package/dist/field-DLSIuMTu.mjs.map +1 -1
  26. package/dist/file-utils-DjNi_3U_.mjs.map +1 -1
  27. package/dist/index-BQ4oi0AI.d.mts +48 -0
  28. package/dist/{index-BbOTbZFf.d.mts → index-BjXN1SdY.d.mts} +2 -2
  29. package/dist/{index-DB8EapT-.d.mts → index-C--7W0UO.d.mts} +5 -5
  30. package/dist/{index-BRvNi5q9.d.mts → index-VJW98BSy.d.mts} +2 -2
  31. package/dist/{index-iy-hNfGp.d.mts → index-nV4ZC_Ve.d.mts} +2 -2
  32. package/dist/{interceptor-CBsqEWDK.mjs → interceptor-DTNS0EtF.mjs} +1 -1
  33. package/dist/{interceptor-CBsqEWDK.mjs.map → interceptor-DTNS0EtF.mjs.map} +1 -1
  34. package/dist/{job-R5C2Hfcc.mjs → job-M3Avv_SV.mjs} +4 -3
  35. package/dist/{job-R5C2Hfcc.mjs.map → job-M3Avv_SV.mjs.map} +1 -1
  36. package/dist/kysely/index.mjs.map +1 -1
  37. package/dist/kysely-type-B8aRz_oC.mjs.map +1 -1
  38. package/dist/logger-DTNAMYGy.mjs.map +1 -1
  39. package/dist/{mock-BP-9O5On.mjs → mock-BfL09ULZ.mjs} +1 -1
  40. package/dist/{mock-BP-9O5On.mjs.map → mock-BfL09ULZ.mjs.map} +1 -1
  41. package/dist/multiline-e3IpANmS.mjs.map +1 -1
  42. package/dist/package-json-6Px8bDpG.mjs.map +1 -1
  43. package/dist/plugin/builtin/enum-constants/index.d.mts +1 -1
  44. package/dist/plugin/builtin/file-utils/index.d.mts +1 -1
  45. package/dist/plugin/builtin/kysely-type/index.d.mts +1 -1
  46. package/dist/plugin/builtin/seed/index.d.mts +1 -1
  47. package/dist/plugin/builtin/seed/index.mjs +1 -1
  48. package/dist/plugin/index.d.mts +2 -2
  49. package/dist/plugin/index.mjs.map +1 -1
  50. package/dist/{repl-editor-CZpLlOBj.mjs → repl-editor-jZ493eQI.mjs} +1 -1
  51. package/dist/{repl-editor-CZpLlOBj.mjs.map → repl-editor-jZ493eQI.mjs.map} +1 -1
  52. package/dist/{runtime-XjP6JMmP.mjs → runtime-oZgK353r.mjs} +484 -132
  53. package/dist/runtime-oZgK353r.mjs.map +1 -0
  54. package/dist/{tailordb-DjlNUV6u.mjs → schema-C5QjYEc-.mjs} +2 -42
  55. package/dist/schema-C5QjYEc-.mjs.map +1 -0
  56. package/dist/secret-file-BHpxGyNf.mjs +65 -0
  57. package/dist/secret-file-BHpxGyNf.mjs.map +1 -0
  58. package/dist/seed/index.mjs.map +1 -1
  59. package/dist/{seed-DrKY5yIF.mjs → seed-DjfAn0BC.mjs} +44 -19
  60. package/dist/seed-DjfAn0BC.mjs.map +1 -0
  61. package/dist/{service-obEU5gSM.mjs → service-DCgJxdg1.mjs} +2 -2
  62. package/dist/{service-obEU5gSM.mjs.map → service-DCgJxdg1.mjs.map} +1 -1
  63. package/dist/{tailor-db-field-Bn8ZC5lK.d.mts → tailor-db-field-4bMLe25-.d.mts} +5 -1
  64. package/dist/telemetry-C13VIFpT.mjs +4 -0
  65. package/dist/{telemetry-DcL8Fsm_.mjs → telemetry-C1Y56L5E.mjs} +1 -1
  66. package/dist/{telemetry-DcL8Fsm_.mjs.map → telemetry-C1Y56L5E.mjs.map} +1 -1
  67. package/dist/types-sir9UPht.mjs.map +1 -1
  68. package/dist/utils/test/index.d.mts +3 -3
  69. package/dist/utils/test/index.mjs +1 -1
  70. package/dist/utils/test/index.mjs.map +1 -1
  71. package/dist/vitest/environment.mjs +1 -1
  72. package/dist/vitest/environment.mjs.map +1 -1
  73. package/dist/vitest/index.mjs +1 -1
  74. package/dist/vitest/index.mjs.map +1 -1
  75. package/dist/vitest/setup.mjs +1 -1
  76. package/dist/vitest/setup.mjs.map +1 -1
  77. package/dist/{workflow.generated-i7PK4fg-.d.mts → workflow.generated-OYAu_6zX.d.mts} +12 -2
  78. package/docs/cli/application.md +4 -0
  79. package/docs/cli/workspace.md +20 -17
  80. package/docs/configuration.md +4 -0
  81. package/docs/generator/builtin.md +35 -4
  82. package/package.json +16 -16
  83. package/postinstall.mjs +1 -1
  84. package/dist/application-C7H7y0hS.mjs.map +0 -1
  85. package/dist/application-Csq5jxYP.mjs +0 -4
  86. package/dist/crashreport-CNSw_BrJ.mjs.map +0 -1
  87. package/dist/index-BXyS7xKC.d.mts +0 -21
  88. package/dist/runtime-XjP6JMmP.mjs.map +0 -1
  89. package/dist/seed-DrKY5yIF.mjs.map +0 -1
  90. package/dist/tailordb-DjlNUV6u.mjs.map +0 -1
  91. package/dist/telemetry-21afNV9_.mjs +0 -4
@@ -1,12 +1,12 @@
1
1
 
2
- import { r as db } from "./tailordb-DjlNUV6u.mjs";
3
- import { $ as FilterSchema, A as FunctionExecution_Status, B as AuthOAuth2Client_GrantType, C as TailorDBType_Permission_Operator, D as IdPLang, E as PipelineResolver_OperationType, F as AuthConnection_Type, H as AuthSCIMAttribute_Type, I as AuthHookPoint, J as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, K as TenantProviderConfig_TenantProviderType, L as AuthIDPConfig_AuthType, M as ExecutorJobStatus, N as ExecutorTargetType, O as IdPPermissionOperator, P as ExecutorTriggerType, Q as Condition_Operator, R as AuthInvokerSchema, S as TailorDBGQLPermission_Permit, T as TailorDBType_PermitAction, U as AuthSCIMAttribute_Uniqueness, V as AuthSCIMAttribute_Mutability, W as AuthSCIMConfig_AuthorizationType, X as Subgraph_ServiceType, Y as ApplicationSchemaUpdateAttemptStatus, Z as ConditionSchema, _ as WorkspacePlatformUserRole, a as fetchMachineUserToken, b as TailorDBGQLPermission_Action, d as initOperatorClient, et as PageDirection, g as OperatorService, h as userAgent, i as fetchAll, k as IdPPermissionPermit, m as resolveStaticWebsiteUrls, o as fetchPaged, p as platformBaseUrl, q as UserProfileProviderConfig_UserProfileProviderType, v as WorkflowExecution_Status, w as TailorDBType_Permission_Permit, x as TailorDBGQLPermission_Operator, y as WorkflowJobExecution_Status, z as AuthOAuth2Client_ClientType } from "./client-DbyKSN1F.mjs";
2
+ import { t as db } from "./schema-C5QjYEc-.mjs";
3
+ import { $ as FilterSchema, A as FunctionExecution_Status, B as AuthOAuth2Client_GrantType, C as TailorDBType_Permission_Operator, D as IdPLang, E as PipelineResolver_OperationType, F as AuthConnection_Type, H as AuthSCIMAttribute_Type, I as AuthHookPoint, J as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, K as TenantProviderConfig_TenantProviderType, L as AuthIDPConfig_AuthType, M as ExecutorJobStatus, N as ExecutorTargetType, O as IdPPermissionOperator, P as ExecutorTriggerType, Q as Condition_Operator, R as AuthInvokerSchema, S as TailorDBGQLPermission_Permit, T as TailorDBType_PermitAction, U as AuthSCIMAttribute_Uniqueness, V as AuthSCIMAttribute_Mutability, W as AuthSCIMConfig_AuthorizationType, X as Subgraph_ServiceType, Y as ApplicationSchemaUpdateAttemptStatus, Z as ConditionSchema, _ as WorkspacePlatformUserRole, a as fetchMachineUserToken, b as TailorDBGQLPermission_Action, d as initOperatorClient, et as PageDirection, g as OperatorService, h as userAgent, i as fetchAll, k as IdPPermissionPermit, m as resolveStaticWebsiteUrls, o as fetchPaged, p as platformBaseUrl, q as UserProfileProviderConfig_UserProfileProviderType, v as WorkflowExecution_Status, w as TailorDBType_Permission_Permit, x as TailorDBGQLPermission_Operator, y as WorkflowJobExecution_Status, z as AuthOAuth2Client_ClientType } from "./client-_kHh0Pip.mjs";
4
4
  import { a as parseBoolean, i as symbols, n as logger, r as styles, t as CIPromptError } from "./logger-DTNAMYGy.mjs";
5
- import { C as loadWorkspaceId, D as writePlatformConfig, S as loadAccessToken, _ as getDistDir, d as buildResolverOperationHookExpr, f as OAuth2ClientSchema, g as createBundleCache, h as loadFilesWithIgnores, m as stringifyFunction, n as generatePluginFilesIfNeeded, p as TailorDBTypeSchema, r as loadApplication, s as createExecutorService, t as defineApplication, u as buildExecutorArgsExpr, v as hashFile, w as readPlatformConfig, y as loadConfig } from "./application-C7H7y0hS.mjs";
5
+ import { C as loadConfigPath, O as writePlatformConfig, S as loadAccessToken, T as readPlatformConfig, _ as getDistDir, d as buildResolverOperationHookExpr, f as OAuth2ClientSchema, g as createBundleCache, h as loadFilesWithIgnores, m as stringifyFunction, n as generatePluginFilesIfNeeded, p as TailorDBTypeSchema, r as loadApplication, s as createExecutorService, t as defineApplication, u as buildExecutorArgsExpr, v as hashFile, w as loadWorkspaceId, y as loadConfig } from "./application-CZMzt9jL.mjs";
6
6
  import { t as multiline } from "./multiline-e3IpANmS.mjs";
7
7
  import { t as readPackageJson } from "./package-json-6Px8bDpG.mjs";
8
- import { n as isCLIError, t as createCLIError } from "./errors-wNQxQQBH.mjs";
9
- import { r as withSpan } from "./telemetry-DcL8Fsm_.mjs";
8
+ import { n as isCLIError, t as createCLIError } from "./errors-pMPXghkO.mjs";
9
+ import { r as withSpan } from "./telemetry-C1Y56L5E.mjs";
10
10
  import { arg, createDefineCommand, defineCommand, runCommand } from "politty";
11
11
  import { z } from "zod";
12
12
  import * as fs$1 from "node:fs";
@@ -23,7 +23,7 @@ import { xdgConfig } from "xdg-basedir";
23
23
  import { Code, ConnectError } from "@connectrpc/connect";
24
24
  import { resolveTSConfig } from "pkg-types";
25
25
  import { ScalarType, create, fromJson, toJson } from "@bufbuild/protobuf";
26
- import * as crypto from "node:crypto";
26
+ import * as crypto$1 from "node:crypto";
27
27
  import { createHash } from "node:crypto";
28
28
  import { ExitPromptError } from "@inquirer/core";
29
29
  import { confirm, input, password } from "@inquirer/prompts";
@@ -31,6 +31,7 @@ import { isCI } from "std-env";
31
31
  import * as rolldown from "rolldown";
32
32
  import * as fs from "node:fs/promises";
33
33
  import { glob } from "node:fs/promises";
34
+ import { parseSync } from "oxc-parser";
34
35
  import * as inflection from "inflection";
35
36
  import { setTimeout as setTimeout$1 } from "timers/promises";
36
37
  import { spawn } from "node:child_process";
@@ -266,6 +267,38 @@ function isVerbose() {
266
267
  */
267
268
  const defineAppCommand = createDefineCommand();
268
269
 
270
+ //#endregion
271
+ //#region src/cli/shared/readonly-guard.ts
272
+ /**
273
+ * Throw a CLIError if the active profile has `readonly: true`.
274
+ *
275
+ * Resolves the active profile in this order:
276
+ * 1. `opts.profile` (CLI flag)
277
+ * 2. `process.env.TAILOR_PLATFORM_PROFILE`
278
+ *
279
+ * If neither is set, no profile is in scope so the call is allowed. This is
280
+ * intentional: `TAILOR_PLATFORM_TOKEN` direct access (CI / machine user) and
281
+ * `--workspace-id` without a profile are out-of-band paths whose authorization
282
+ * is governed by the bearer token itself, not by the local profile flag.
283
+ *
284
+ * If the resolved profile cannot be found in the config, this function returns
285
+ * silently and lets downstream loaders surface the not-found error.
286
+ * @param opts - Options
287
+ * @param opts.profile - Optional explicit profile name from command args
288
+ */
289
+ async function assertWritable(opts) {
290
+ const profileName = opts?.profile || process.env.TAILOR_PLATFORM_PROFILE;
291
+ if (!profileName) return;
292
+ const profile = (await readPlatformConfig()).profiles[profileName];
293
+ if (!profile || profile.readonly !== true) return;
294
+ throw createCLIError({
295
+ code: "PROFILE_READONLY",
296
+ message: `Profile "${profileName}" is read-only.`,
297
+ details: "This profile blocks platform-state mutations (apply, create/update/delete, deploy, etc.). Application-data operations remain available because their permissions are governed by the machine user.",
298
+ suggestion: `Use a different profile, unset TAILOR_PLATFORM_PROFILE, or run 'tailor-sdk profile update ${profileName} --permission write'.`
299
+ });
300
+ }
301
+
269
302
  //#endregion
270
303
  //#region src/cli/commands/api/api-call.ts
271
304
  /**
@@ -567,6 +600,7 @@ Values already present in \`--body\` are never overridden. If a value cannot be
567
600
  })
568
601
  }).strict(),
569
602
  run: async (args) => {
603
+ await assertWritable({ profile: args.profile });
570
604
  const methodName = extractMethodName(args.endpoint);
571
605
  const method = getMethodDescriptor(methodName);
572
606
  const parsedBody = parseBodyAsObject(args.body);
@@ -1443,6 +1477,11 @@ function trnPrefix(workspaceId) {
1443
1477
  }
1444
1478
  const sdkNameLabelKey = "sdk-name";
1445
1479
  const sdkVersionLabelKey = "sdk-version";
1480
+ const sdkAppIdLabelKey = "sdk-app-id";
1481
+ const appIdLabelPrefix = "app-";
1482
+ function toAppIdLabelValue(appId) {
1483
+ return `${appIdLabelPrefix}${appId}`;
1484
+ }
1446
1485
  /**
1447
1486
  * Check whether existing metadata was produced by the current SDK version.
1448
1487
  * @param existingLabels - Labels currently stored on the remote resource
@@ -1453,13 +1492,33 @@ function hasMatchingSdkVersion(existingLabels, desiredLabels) {
1453
1492
  return existingLabels?.[sdkVersionLabelKey] === desiredLabels?.[sdkVersionLabelKey];
1454
1493
  }
1455
1494
  /**
1495
+ * Determine whether a remote resource is owned by the given application.
1496
+ * When the resource carries an `sdk-app-id`, ownership is decided strictly
1497
+ * by id match — a resource explicitly tagged with another app's id is
1498
+ * NOT ours even if the legacy sdk-name happens to match. Resources without
1499
+ * `sdk-app-id` (legacy) fall back to sdk-name comparison.
1500
+ * @param labels - Labels currently stored on the remote resource
1501
+ * @param appName - Application name from the local config
1502
+ * @param appId - Stable application id from the local config (when present)
1503
+ * @returns True when the resource is owned by the application
1504
+ */
1505
+ function isOwnedByApp(labels, appName, appId) {
1506
+ if (!labels) return false;
1507
+ const labelAppId = labels[sdkAppIdLabelKey];
1508
+ if (labelAppId) return appId !== void 0 && labelAppId === toAppIdLabelValue(appId);
1509
+ return labels[sdkNameLabelKey] === appName;
1510
+ }
1511
+ /**
1456
1512
  * Build metadata request with SDK labels.
1457
- * @param trn - Target TRN
1458
- * @param appName - Application name label
1459
- * @param existingLabels - Existing labels to preserve (optional)
1513
+ * @param params - Parameters for building the metadata request
1514
+ * @param params.trn - Target TRN
1515
+ * @param params.appName - Application name label
1516
+ * @param params.appId - Stable application id label (when managed by SDK)
1517
+ * @param params.existingLabels - Existing labels to preserve (optional)
1460
1518
  * @returns Metadata request
1461
1519
  */
1462
- async function buildMetaRequest(trn, appName, existingLabels) {
1520
+ async function buildMetaRequest(params) {
1521
+ const { trn, appName, appId, existingLabels } = params;
1463
1522
  const packageJson = await readPackageJson();
1464
1523
  const sdkVersion = packageJson.version ? `v${packageJson.version.replace(/\./g, "-")}` : "unknown";
1465
1524
  return {
@@ -1467,7 +1526,8 @@ async function buildMetaRequest(trn, appName, existingLabels) {
1467
1526
  labels: {
1468
1527
  ...existingLabels ?? {},
1469
1528
  [sdkNameLabelKey]: appName,
1470
- [sdkVersionLabelKey]: sdkVersion
1529
+ [sdkVersionLabelKey]: sdkVersion,
1530
+ ...appId ? { [sdkAppIdLabelKey]: toAppIdLabelValue(appId) } : {}
1471
1531
  }
1472
1532
  };
1473
1533
  }
@@ -1568,11 +1628,20 @@ async function planApplication(context) {
1568
1628
  }
1569
1629
  });
1570
1630
  if (forRemoval) {
1571
- if (existingApplications.some((app) => app.name === application.name)) changeSet.deletes.push({
1572
- name: application.name,
1631
+ const ownedAppNames = /* @__PURE__ */ new Set();
1632
+ if (existingApplications.some((app) => app.name === application.name)) ownedAppNames.add(application.name);
1633
+ if (application.id) {
1634
+ const others = existingApplications.filter((app) => !ownedAppNames.has(app.name));
1635
+ const owned = await Promise.all(others.map(async (app) => {
1636
+ return isOwnedByApp(await fetchAppLabels(client, workspaceId, app.name), application.name, application.id) ? app.name : null;
1637
+ }));
1638
+ for (const name of owned) if (name) ownedAppNames.add(name);
1639
+ }
1640
+ for (const name of ownedAppNames) changeSet.deletes.push({
1641
+ name,
1573
1642
  request: {
1574
1643
  workspaceId,
1575
- applicationName: application.name
1644
+ applicationName: name
1576
1645
  }
1577
1646
  });
1578
1647
  return changeSet;
@@ -1602,7 +1671,11 @@ async function planApplication(context) {
1602
1671
  });
1603
1672
  if (idpConfigs.length > 0) authIdpConfigName = idpConfigs[0].name;
1604
1673
  }
1605
- const metaRequest = await buildMetaRequest(trn$6(workspaceId, application.name), application.name);
1674
+ const metaRequest = await buildMetaRequest({
1675
+ trn: trn$6(workspaceId, application.name),
1676
+ appName: application.name,
1677
+ appId: application.id
1678
+ });
1606
1679
  const expectedLocalWebsites = new Set(application.staticWebsiteServices.map((website) => website.name));
1607
1680
  const resolvedCors = await resolveStaticWebsiteUrls(client, workspaceId, application.config.cors, "CORS", { expectedLocalNames: expectedLocalWebsites });
1608
1681
  const desired = normalizeComparableApplication(application, authNamespace, authIdpConfigName, resolvedCors);
@@ -1617,9 +1690,22 @@ async function planApplication(context) {
1617
1690
  disableIntrospection: application.config.disableIntrospection
1618
1691
  };
1619
1692
  const existing = existingApplications.find((app) => app.name === application.name);
1693
+ if (application.id) {
1694
+ const otherApps = existingApplications.filter((app) => app.name !== application.name);
1695
+ const renamedAway = await Promise.all(otherApps.map(async (app) => {
1696
+ return isOwnedByApp(await fetchAppLabels(client, workspaceId, app.name), application.name, application.id) ? app.name : null;
1697
+ }));
1698
+ for (const name of renamedAway) if (name) changeSet.deletes.push({
1699
+ name,
1700
+ request: {
1701
+ workspaceId,
1702
+ applicationName: name
1703
+ }
1704
+ });
1705
+ }
1620
1706
  if (existing) {
1621
- const { metadata } = await client.getMetadata({ trn: trn$6(workspaceId, application.name) });
1622
- if (metadata?.labels?.["sdk-name"] === application.name && hasMatchingSdkVersion(metadata?.labels, metaRequest.labels) && areApplicationsEqual(existing, desired)) changeSet.unchanged.push({ name: application.name });
1707
+ const labels = await fetchAppLabels(client, workspaceId, application.name);
1708
+ if (isOwnedByApp(labels, application.name, application.id) && hasMatchingSdkVersion(labels, metaRequest.labels) && areApplicationsEqual(existing, desired)) changeSet.unchanged.push({ name: application.name });
1623
1709
  else changeSet.updates.push({
1624
1710
  name: application.name,
1625
1711
  request,
@@ -1632,6 +1718,15 @@ async function planApplication(context) {
1632
1718
  });
1633
1719
  return changeSet;
1634
1720
  }
1721
+ async function fetchAppLabels(client, workspaceId, appName) {
1722
+ try {
1723
+ const { metadata } = await client.getMetadata({ trn: trn$6(workspaceId, appName) });
1724
+ return metadata?.labels;
1725
+ } catch (error) {
1726
+ if (error instanceof ConnectError && error.code === Code.NotFound) return;
1727
+ throw error;
1728
+ }
1729
+ }
1635
1730
  function protoSubgraph(subgraph) {
1636
1731
  let serviceType;
1637
1732
  switch (subgraph.Type) {
@@ -1757,10 +1852,11 @@ function hasNonSecretFieldChanged(existing, desired) {
1757
1852
  * @param client - Operator client instance
1758
1853
  * @param workspaceId - Workspace ID
1759
1854
  * @param appName - Application name for ownership
1855
+ * @param appId - Stable application id (when managed by SDK)
1760
1856
  * @param auths - Auth services with connection configs
1761
1857
  * @returns Planned changes for auth connections
1762
1858
  */
1763
- async function planAuthConnections(client, workspaceId, appName, auths) {
1859
+ async function planAuthConnections(client, workspaceId, appName, appId, auths) {
1764
1860
  const changeSet = createChangeSet("Auth connections");
1765
1861
  const conflicts = [];
1766
1862
  const unmanaged = [];
@@ -1787,7 +1883,8 @@ async function planAuthConnections(client, workspaceId, appName, auths) {
1787
1883
  const { metadata } = await client.getMetadata({ trn: connectionTrn(workspaceId, resource.name) });
1788
1884
  existingConnections[resource.name] = {
1789
1885
  resource,
1790
- label: metadata?.labels[sdkNameLabelKey]
1886
+ label: metadata?.labels[sdkNameLabelKey],
1887
+ allLabels: metadata?.labels
1791
1888
  };
1792
1889
  } catch (error) {
1793
1890
  if (error instanceof ConnectError && error.code === Code.InvalidArgument) {
@@ -1802,17 +1899,23 @@ async function planAuthConnections(client, workspaceId, appName, auths) {
1802
1899
  const state = loadSecretsState();
1803
1900
  for (const [name, config] of Object.entries(desiredConnections)) {
1804
1901
  const existing = existingConnections[name];
1805
- const metaRequest = metadataSupported ? await buildMetaRequest(connectionTrn(workspaceId, name), appName) : void 0;
1902
+ const metaRequest = metadataSupported ? await buildMetaRequest({
1903
+ trn: connectionTrn(workspaceId, name),
1904
+ appName,
1905
+ appId
1906
+ }) : void 0;
1806
1907
  if (existing) {
1807
- if (metadataSupported && !existing.label) unmanaged.push({
1808
- resourceType: "Auth connection",
1809
- resourceName: name
1810
- });
1811
- else if (existing.label && existing.label !== appName) conflicts.push({
1812
- resourceType: "Auth connection",
1813
- resourceName: name,
1814
- currentOwner: existing.label
1815
- });
1908
+ if (!isOwnedByApp(existing.allLabels, appName, appId)) {
1909
+ if (metadataSupported && !existing.label) unmanaged.push({
1910
+ resourceType: "Auth connection",
1911
+ resourceName: name
1912
+ });
1913
+ else if (existing.label) conflicts.push({
1914
+ resourceType: "Auth connection",
1915
+ resourceName: name,
1916
+ currentOwner: existing.label
1917
+ });
1918
+ }
1816
1919
  const currentHash = hashConnectionConfig(config);
1817
1920
  const storedHash = state.connections?.[name];
1818
1921
  if (hasNonSecretFieldChanged(existing.resource, config) || currentHash !== storedHash) changeSet.replaces.push({
@@ -1834,11 +1937,12 @@ async function planAuthConnections(client, workspaceId, appName, auths) {
1834
1937
  }
1835
1938
  for (const [name, entry] of Object.entries(existingConnections)) {
1836
1939
  if (!entry) continue;
1837
- if (entry.label && entry.label !== appName) {
1940
+ const owned = isOwnedByApp(entry.allLabels, appName, appId);
1941
+ if (entry.label && !owned) {
1838
1942
  resourceOwners.add(entry.label);
1839
1943
  continue;
1840
1944
  }
1841
- if (entry.label === appName || !metadataSupported) changeSet.deletes.push({
1945
+ if (owned || !metadataSupported) changeSet.deletes.push({
1842
1946
  name,
1843
1947
  request: {
1844
1948
  workspaceId,
@@ -1934,7 +2038,7 @@ const CHUNK_SIZE = 64 * 1024;
1934
2038
  * @returns Hex-encoded SHA-256 hash
1935
2039
  */
1936
2040
  function computeContentHash(content) {
1937
- return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
2041
+ return crypto$1.createHash("sha256").update(content, "utf-8").digest("hex");
1938
2042
  }
1939
2043
  function functionRegistryTrn$1(workspaceId, name) {
1940
2044
  return `trn:v1:workspace:${workspaceId}:function_registry:${name}`;
@@ -2103,10 +2207,11 @@ function filterBundledWorkflowJobs(jobs, usedJobNames) {
2103
2207
  * @param client - Operator client instance
2104
2208
  * @param workspaceId - Workspace ID
2105
2209
  * @param appName - Application name
2210
+ * @param appId - Stable application id (when managed by SDK)
2106
2211
  * @param entries - Desired function entries
2107
2212
  * @returns Planned changes
2108
2213
  */
2109
- async function planFunctionRegistry(client, workspaceId, appName, entries) {
2214
+ async function planFunctionRegistry(client, workspaceId, appName, appId, entries) {
2110
2215
  const changeSet = createChangeSet("Function registry");
2111
2216
  const conflicts = [];
2112
2217
  const unmanaged = [];
@@ -2138,19 +2243,23 @@ async function planFunctionRegistry(client, workspaceId, appName, entries) {
2138
2243
  }));
2139
2244
  for (const entry of entries) {
2140
2245
  const existing = existingMap[entry.name];
2141
- const metaRequest = await buildMetaRequest(functionRegistryTrn$1(workspaceId, entry.name), appName);
2246
+ const metaRequest = await buildMetaRequest({
2247
+ trn: functionRegistryTrn$1(workspaceId, entry.name),
2248
+ appName,
2249
+ appId
2250
+ });
2142
2251
  if (existing) {
2143
- const isManagedByApp = existing.label === appName;
2144
- if (!existing.label) unmanaged.push({
2252
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
2253
+ if (!owned) if (!existing.label) unmanaged.push({
2145
2254
  resourceType: "Function registry",
2146
2255
  resourceName: entry.name
2147
2256
  });
2148
- else if (existing.label !== appName) conflicts.push({
2257
+ else conflicts.push({
2149
2258
  resourceType: "Function registry",
2150
2259
  resourceName: entry.name,
2151
2260
  currentOwner: existing.label
2152
2261
  });
2153
- if (existing.resource.contentHash === entry.contentHash && isManagedByApp && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: entry.name });
2262
+ if (existing.resource.contentHash === entry.contentHash && owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: entry.name });
2154
2263
  else changeSet.updates.push({
2155
2264
  name: entry.name,
2156
2265
  entry,
@@ -2166,8 +2275,9 @@ async function planFunctionRegistry(client, workspaceId, appName, entries) {
2166
2275
  for (const [name, existing] of Object.entries(existingMap)) {
2167
2276
  if (!existing) continue;
2168
2277
  const label = existing.label;
2169
- if (label && label !== appName) resourceOwners.add(label);
2170
- if (label === appName) changeSet.deletes.push({
2278
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
2279
+ if (label && !owned) resourceOwners.add(label);
2280
+ if (owned) changeSet.deletes.push({
2171
2281
  name,
2172
2282
  workspaceId
2173
2283
  });
@@ -2659,7 +2769,7 @@ async function applyIdP(client, result, phase = "create-update") {
2659
2769
  async function planIdP(context) {
2660
2770
  const { client, workspaceId, application, forRemoval, forceApplyAll = false, idpUserTriggerTargets } = context;
2661
2771
  const idps = forRemoval ? [] : application.idpServices;
2662
- const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set());
2772
+ const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$3(client, workspaceId, application.name, application.id, idps, idpUserTriggerTargets ?? /* @__PURE__ */ new Set());
2663
2773
  return {
2664
2774
  changeSet: {
2665
2775
  service: serviceChangeSet,
@@ -2746,7 +2856,7 @@ function areIdPServicesEqual(existing, desired) {
2746
2856
  permission: normalizeComparablePermission(existing.permission)
2747
2857
  }), desired);
2748
2858
  }
2749
- async function planServices$3(client, workspaceId, appName, idps, idpUserTriggerTargets) {
2859
+ async function planServices$3(client, workspaceId, appName, appId, idps, idpUserTriggerTargets) {
2750
2860
  const changeSet = createChangeSet("IdP services");
2751
2861
  const conflicts = [];
2752
2862
  const unmanaged = [];
@@ -2777,7 +2887,11 @@ async function planServices$3(client, workspaceId, appName, idps, idpUserTrigger
2777
2887
  for (const idp of idps) {
2778
2888
  const namespaceName = idp.name;
2779
2889
  const existing = existingServices[namespaceName];
2780
- const metaRequest = await buildMetaRequest(trn$5(workspaceId, namespaceName), appName);
2890
+ const metaRequest = await buildMetaRequest({
2891
+ trn: trn$5(workspaceId, namespaceName),
2892
+ appName,
2893
+ appId
2894
+ });
2781
2895
  let authorization;
2782
2896
  switch (idp.authorization) {
2783
2897
  case "insecure":
@@ -2823,17 +2937,17 @@ async function planServices$3(client, workspaceId, appName, idps, idpUserTrigger
2823
2937
  permission: protoPermission
2824
2938
  };
2825
2939
  if (existing) {
2826
- const isManagedByApp = existing.label === appName;
2827
- if (!existing.label) unmanaged.push({
2940
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
2941
+ if (!owned) if (!existing.label) unmanaged.push({
2828
2942
  resourceType: "IdP service",
2829
2943
  resourceName: idp.name
2830
2944
  });
2831
- else if (existing.label !== appName) conflicts.push({
2945
+ else conflicts.push({
2832
2946
  resourceType: "IdP service",
2833
2947
  resourceName: idp.name,
2834
2948
  currentOwner: existing.label
2835
2949
  });
2836
- if (isManagedByApp && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({ name: namespaceName });
2950
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areIdPServicesEqual(existing.resource, desired)) changeSet.unchanged.push({ name: namespaceName });
2837
2951
  else changeSet.updates.push({
2838
2952
  name: namespaceName,
2839
2953
  request,
@@ -2847,9 +2961,11 @@ async function planServices$3(client, workspaceId, appName, idps, idpUserTrigger
2847
2961
  });
2848
2962
  }
2849
2963
  Object.entries(existingServices).forEach(([namespaceName]) => {
2850
- const label = existingServices[namespaceName]?.label;
2851
- if (label && label !== appName) resourceOwners.add(label);
2852
- if (label === appName) changeSet.deletes.push({
2964
+ const entry = existingServices[namespaceName];
2965
+ const label = entry?.label;
2966
+ const owned = isOwnedByApp(entry?.allLabels, appName, appId);
2967
+ if (label && !owned) resourceOwners.add(label);
2968
+ if (owned) changeSet.deletes.push({
2853
2969
  name: namespaceName,
2854
2970
  request: {
2855
2971
  workspaceId,
@@ -3097,7 +3213,7 @@ async function planAuth(context) {
3097
3213
  await application.authService.resolveNamespaces();
3098
3214
  auths.push(application.authService);
3099
3215
  }
3100
- const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$2(client, workspaceId, application.name, auths, forceApplyAll);
3216
+ const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$2(client, workspaceId, application.name, application.id, auths, forceApplyAll);
3101
3217
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
3102
3218
  const expectedLocalWebsites = new Set(application.staticWebsiteServices.map((website) => website.name));
3103
3219
  const [idpConfigChangeSet, userProfileConfigChangeSet, tenantConfigChangeSet, machineUserChangeSet, authHookChangeSet, oauth2ClientChangeSet, scimChangeSet, scimResourceChangeSet, connectionResult] = await Promise.all([
@@ -3109,7 +3225,7 @@ async function planAuth(context) {
3109
3225
  planOAuth2Clients(client, workspaceId, auths, deletedServices, expectedLocalWebsites, forceApplyAll),
3110
3226
  planSCIMConfigs(client, workspaceId, auths, deletedServices),
3111
3227
  planSCIMResources(client, workspaceId, auths, deletedServices),
3112
- planAuthConnections(client, workspaceId, application.name, auths)
3228
+ planAuthConnections(client, workspaceId, application.name, application.id, auths)
3113
3229
  ]);
3114
3230
  return {
3115
3231
  changeSet: {
@@ -3132,7 +3248,7 @@ async function planAuth(context) {
3132
3248
  function trn$4(workspaceId, name) {
3133
3249
  return `trn:v1:workspace:${workspaceId}:auth:${name}`;
3134
3250
  }
3135
- async function planServices$2(client, workspaceId, appName, auths, forceApplyAll = false) {
3251
+ async function planServices$2(client, workspaceId, appName, appId, auths, forceApplyAll = false) {
3136
3252
  const changeSet = createChangeSet("Auth services");
3137
3253
  const conflicts = [];
3138
3254
  const unmanaged = [];
@@ -3156,30 +3272,35 @@ async function planServices$2(client, workspaceId, appName, auths, forceApplyAll
3156
3272
  const { metadata } = await client.getMetadata({ trn: trn$4(workspaceId, resource.namespace.name) });
3157
3273
  existingServices[resource.namespace.name] = {
3158
3274
  resource,
3159
- label: metadata?.labels[sdkNameLabelKey]
3275
+ label: metadata?.labels[sdkNameLabelKey],
3276
+ allLabels: metadata?.labels
3160
3277
  };
3161
3278
  }));
3162
3279
  for (const auth of auths) {
3163
3280
  const { parsedConfig: config } = auth;
3164
3281
  const existing = existingServices[config.name];
3165
- const metaRequest = await buildMetaRequest(trn$4(workspaceId, config.name), appName);
3282
+ const metaRequest = await buildMetaRequest({
3283
+ trn: trn$4(workspaceId, config.name),
3284
+ appName,
3285
+ appId
3286
+ });
3166
3287
  const request = {
3167
3288
  workspaceId,
3168
3289
  namespaceName: config.name,
3169
3290
  publishSessionEvents: config.publishSessionEvents
3170
3291
  };
3171
3292
  if (existing) {
3172
- const isManagedByApp = existing.label === appName;
3173
- if (!existing.label) unmanaged.push({
3293
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
3294
+ if (!owned) if (!existing.label) unmanaged.push({
3174
3295
  resourceType: "Auth service",
3175
3296
  resourceName: config.name
3176
3297
  });
3177
- else if (existing.label !== appName) conflicts.push({
3298
+ else conflicts.push({
3178
3299
  resourceType: "Auth service",
3179
3300
  resourceName: config.name,
3180
3301
  currentOwner: existing.label
3181
3302
  });
3182
- if (!forceApplyAll && existing.resource.publishSessionEvents === (config.publishSessionEvents ?? false) && isManagedByApp) changeSet.unchanged.push({ name: config.name });
3303
+ if (!forceApplyAll && existing.resource.publishSessionEvents === (config.publishSessionEvents ?? false) && owned) changeSet.unchanged.push({ name: config.name });
3183
3304
  else changeSet.updates.push({
3184
3305
  name: config.name,
3185
3306
  request,
@@ -3193,9 +3314,11 @@ async function planServices$2(client, workspaceId, appName, auths, forceApplyAll
3193
3314
  });
3194
3315
  }
3195
3316
  Object.entries(existingServices).forEach(([namespaceName]) => {
3196
- const label = existingServices[namespaceName]?.label;
3197
- if (label && label !== appName) resourceOwners.add(label);
3198
- if (label === appName) changeSet.deletes.push({
3317
+ const entry = existingServices[namespaceName];
3318
+ const label = entry?.label;
3319
+ const owned = isOwnedByApp(entry?.allLabels, appName, appId);
3320
+ if (label && !owned) resourceOwners.add(label);
3321
+ if (owned) changeSet.deletes.push({
3199
3322
  name: namespaceName,
3200
3323
  request: {
3201
3324
  workspaceId,
@@ -4159,10 +4282,99 @@ async function planAuthHooks(client, workspaceId, auths, deletedServices, forceA
4159
4282
  return changeSet;
4160
4283
  }
4161
4284
 
4285
+ //#endregion
4286
+ //#region src/cli/commands/deploy/config-id-injector.ts
4287
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4288
+ function findDefineConfigCalls(node, results) {
4289
+ if (!node || typeof node !== "object") return;
4290
+ const n = node;
4291
+ if (n.type === "CallExpression") {
4292
+ const ce = n;
4293
+ if (ce.callee.type === "Identifier" && ce.callee.name === "defineConfig") {
4294
+ const arg = ce.arguments[0];
4295
+ const configObj = arg && arg.type === "ObjectExpression" ? arg : null;
4296
+ results.push({
4297
+ callExpr: ce,
4298
+ configObj
4299
+ });
4300
+ }
4301
+ }
4302
+ for (const key of Object.keys(n)) {
4303
+ const child = n[key];
4304
+ if (Array.isArray(child)) for (const c of child) findDefineConfigCalls(c, results);
4305
+ else if (child && typeof child === "object") findDefineConfigCalls(child, results);
4306
+ }
4307
+ }
4308
+ function findIdProperty(obj) {
4309
+ for (const prop of obj.properties) {
4310
+ if (prop.type !== "Property") continue;
4311
+ if ((prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? prop.key.value : null) === "id") return prop;
4312
+ }
4313
+ return null;
4314
+ }
4315
+ /**
4316
+ * Ensure `tailor.config.ts` has an `id` property on the `defineConfig({...})`
4317
+ * argument. Generates a UUID when missing and writes it back to the file.
4318
+ * Returns null when the file does not contain a `defineConfig()` call (e.g.
4319
+ * a wrapper that re-exports another config).
4320
+ * @param configPath - Absolute path to the config file
4321
+ * @returns Resolved id and whether it was newly injected, or null if skipped
4322
+ */
4323
+ async function ensureConfigId(configPath) {
4324
+ const source = await fs$1.promises.readFile(configPath, "utf-8");
4325
+ const { program } = parseSync(configPath, source);
4326
+ const calls = [];
4327
+ findDefineConfigCalls(program, calls);
4328
+ if (calls.length === 0) return null;
4329
+ if (calls.length > 1) throw new Error(`Multiple defineConfig() calls found in ${configPath}. Only one is supported.`);
4330
+ const { configObj } = calls[0];
4331
+ if (!configObj) throw new Error(`defineConfig() argument must be an inline object literal in ${configPath} so the SDK can manage the 'id' field.`);
4332
+ const idProp = findIdProperty(configObj);
4333
+ if (idProp) {
4334
+ const value = idProp.value;
4335
+ if (value.type !== "Literal") throw new Error(`'id' field in ${configPath} must be a string literal. To use this config for a separate app, delete it.`);
4336
+ const literalValue = value.value;
4337
+ if (typeof literalValue !== "string" || literalValue === "") throw new Error(`'id' field in ${configPath} must be a non-empty string literal. To use this config for a separate app, delete it.`);
4338
+ if (!uuidRegex.test(literalValue)) throw new Error(`'id' field in ${configPath} must be a UUID. To use this config for a separate app, delete it.`);
4339
+ return {
4340
+ id: literalValue,
4341
+ injected: false
4342
+ };
4343
+ }
4344
+ const id = crypto.randomUUID();
4345
+ const newSource = insertIdProperty(source, configObj, id);
4346
+ await fs$1.promises.writeFile(configPath, newSource, "utf-8");
4347
+ logger.info(`Generated app id and wrote to ${configPath}: ${id}`);
4348
+ return {
4349
+ id,
4350
+ injected: true
4351
+ };
4352
+ }
4353
+ const idComment = "// SDK-managed app id — do not edit, except when copying this config to a separate app.";
4354
+ function insertIdProperty(source, configObj, id) {
4355
+ const idLiteral = `id: ${JSON.stringify(id)}`;
4356
+ if (configObj.properties.length > 0) {
4357
+ const firstProp = configObj.properties[0];
4358
+ const lineStart = source.lastIndexOf("\n", firstProp.start - 1) + 1;
4359
+ const indent = source.slice(lineStart, firstProp.start);
4360
+ const insertion = `${idComment}\n${indent}${idLiteral},\n${indent}`;
4361
+ return source.slice(0, firstProp.start) + insertion + source.slice(firstProp.start);
4362
+ }
4363
+ const openBracePos = configObj.start + 1;
4364
+ const braceLineStart = source.lastIndexOf("\n", configObj.start) + 1;
4365
+ const baseIndent = source.slice(braceLineStart).match(/^[\t ]*/)?.[0] ?? "";
4366
+ const innerIndent = `${baseIndent} `;
4367
+ const insertion = `\n${innerIndent}${idComment}\n${innerIndent}${idLiteral},\n${baseIndent}`;
4368
+ return source.slice(0, openBracePos) + insertion + source.slice(openBracePos);
4369
+ }
4370
+
4162
4371
  //#endregion
4163
4372
  //#region src/cli/commands/deploy/confirm.ts
4164
4373
  /**
4165
4374
  * Confirm reassignment of resources when owner conflicts are detected.
4375
+ * Splits into two scenarios: id regeneration (same sdk-name, different
4376
+ * sdk-app-id) and name mismatch (different sdk-name). Each gets its own
4377
+ * prompt because the user-facing meaning is different.
4166
4378
  * @param conflicts - Detected owner conflicts
4167
4379
  * @param appName - Target application name
4168
4380
  * @param yes - Whether to auto-confirm without prompting
@@ -4170,6 +4382,30 @@ async function planAuthHooks(client, workspaceId, auths, deletedServices, forceA
4170
4382
  */
4171
4383
  async function confirmOwnerConflict(conflicts, appName, yes) {
4172
4384
  if (conflicts.length === 0) return;
4385
+ const idRegenerated = conflicts.filter((c) => c.currentOwner === appName);
4386
+ const nameMismatches = conflicts.filter((c) => c.currentOwner !== appName);
4387
+ if (idRegenerated.length > 0) await confirmIdRegeneration(idRegenerated, appName, yes);
4388
+ if (nameMismatches.length > 0) await confirmNameMismatch(nameMismatches, appName, yes);
4389
+ }
4390
+ async function confirmIdRegeneration(conflicts, appName, yes) {
4391
+ logger.warn(`Application id was regenerated for "${appName}":`);
4392
+ logger.log(" These resources still carry the previous id.");
4393
+ logger.newline();
4394
+ logger.log(` ${styles.info("Resources")}:`);
4395
+ for (const c of conflicts) logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
4396
+ if (yes) {
4397
+ logger.success("Re-tagging resources with the new id (--yes flag specified)...", { mode: "plain" });
4398
+ return;
4399
+ }
4400
+ if (!await prompt.confirm({
4401
+ message: `Re-tag these resources with the new id for "${appName}"?\n${styles.dim("(The id in tailor.config.ts was removed since the previous deploy, so a new one was generated)")}`,
4402
+ default: false
4403
+ })) throw new Error(multiline`
4404
+ Apply cancelled. Resources remain tagged with the previous id.
4405
+ To override, run again and confirm, or use --yes flag.
4406
+ `);
4407
+ }
4408
+ async function confirmNameMismatch(conflicts, appName, yes) {
4173
4409
  const currentOwners = [...new Set(conflicts.map((c) => c.currentOwner))];
4174
4410
  logger.warn("Application name mismatch detected:");
4175
4411
  logger.log(` ${styles.warning("Current application(s)")}: ${currentOwners.map((o) => styles.bold(`"${o}"`)).join(", ")}`);
@@ -4329,19 +4565,24 @@ async function planExecutor(context) {
4329
4565
  const executors = forRemoval ? {} : await application.executorService?.loadExecutors() ?? {};
4330
4566
  for (const executor of Object.values(executors)) {
4331
4567
  const existing = existingExecutors[executor.name];
4332
- const metaRequest = await buildMetaRequest(trn$3(workspaceId, executor.name), application.name);
4568
+ const metaRequest = await buildMetaRequest({
4569
+ trn: trn$3(workspaceId, executor.name),
4570
+ appName: application.name,
4571
+ appId: application.id
4572
+ });
4333
4573
  const desiredExecutor = protoExecutor(application, executor);
4334
4574
  if (existing) {
4335
- if (!existing.label) unmanaged.push({
4575
+ const owned = isOwnedByApp(existing.allLabels, application.name, application.id);
4576
+ if (!owned) if (!existing.label) unmanaged.push({
4336
4577
  resourceType: "Executor",
4337
4578
  resourceName: executor.name
4338
4579
  });
4339
- else if (existing.label !== application.name) conflicts.push({
4580
+ else conflicts.push({
4340
4581
  resourceType: "Executor",
4341
4582
  resourceName: executor.name,
4342
4583
  currentOwner: existing.label
4343
4584
  });
4344
- if (existing.label === application.name && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areExecutorsEqual(existing.resource, desiredExecutor)) changeSet.unchanged.push({ name: executor.name });
4585
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areExecutorsEqual(existing.resource, desiredExecutor)) changeSet.unchanged.push({ name: executor.name });
4345
4586
  else changeSet.updates.push({
4346
4587
  name: executor.name,
4347
4588
  request: {
@@ -4361,9 +4602,11 @@ async function planExecutor(context) {
4361
4602
  });
4362
4603
  }
4363
4604
  Object.entries(existingExecutors).forEach(([name]) => {
4364
- const label = existingExecutors[name]?.label;
4365
- if (label && label !== application.name) resourceOwners.add(label);
4366
- if (label === application.name) changeSet.deletes.push({
4605
+ const entry = existingExecutors[name];
4606
+ const label = entry?.label;
4607
+ const owned = isOwnedByApp(entry?.allLabels, application.name, application.id);
4608
+ if (label && !owned) resourceOwners.add(label);
4609
+ if (owned) changeSet.deletes.push({
4367
4610
  name,
4368
4611
  request: {
4369
4612
  workspaceId,
@@ -4722,7 +4965,7 @@ async function planPipeline(context) {
4722
4965
  pipelines.push(pipeline);
4723
4966
  }
4724
4967
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
4725
- const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$1(client, workspaceId, application.name, pipelines);
4968
+ const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices$1(client, workspaceId, application.name, application.id, pipelines);
4726
4969
  const { changeSet: resolverChangeSet } = await planResolvers(client, workspaceId, pipelines, executors, serviceChangeSet.deletes.map((del) => del.name), application.env, application.authService?.config.name, forceApplyAll);
4727
4970
  return {
4728
4971
  changeSet: {
@@ -4737,7 +4980,7 @@ async function planPipeline(context) {
4737
4980
  function trn$2(workspaceId, name) {
4738
4981
  return `trn:v1:workspace:${workspaceId}:pipeline:${name}`;
4739
4982
  }
4740
- async function planServices$1(client, workspaceId, appName, pipelines) {
4983
+ async function planServices$1(client, workspaceId, appName, appId, pipelines) {
4741
4984
  const changeSet = createChangeSet("Pipeline services");
4742
4985
  const conflicts = [];
4743
4986
  const unmanaged = [];
@@ -4767,18 +5010,23 @@ async function planServices$1(client, workspaceId, appName, pipelines) {
4767
5010
  }));
4768
5011
  for (const pipeline of pipelines) {
4769
5012
  const existing = existingServices[pipeline.namespace];
4770
- const metaRequest = await buildMetaRequest(trn$2(workspaceId, pipeline.namespace), appName);
5013
+ const metaRequest = await buildMetaRequest({
5014
+ trn: trn$2(workspaceId, pipeline.namespace),
5015
+ appName,
5016
+ appId
5017
+ });
4771
5018
  if (existing) {
4772
- if (!existing.label) unmanaged.push({
5019
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
5020
+ if (!owned) if (!existing.label) unmanaged.push({
4773
5021
  resourceType: "Pipeline service",
4774
5022
  resourceName: pipeline.namespace
4775
5023
  });
4776
- else if (existing.label !== appName) conflicts.push({
5024
+ else conflicts.push({
4777
5025
  resourceType: "Pipeline service",
4778
5026
  resourceName: pipeline.namespace,
4779
5027
  currentOwner: existing.label
4780
5028
  });
4781
- if (existing.label === appName && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: pipeline.namespace });
5029
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) changeSet.unchanged.push({ name: pipeline.namespace });
4782
5030
  else changeSet.updates.push({
4783
5031
  name: pipeline.namespace,
4784
5032
  request: {
@@ -4798,9 +5046,11 @@ async function planServices$1(client, workspaceId, appName, pipelines) {
4798
5046
  });
4799
5047
  }
4800
5048
  Object.entries(existingServices).forEach(([namespaceName]) => {
4801
- const label = existingServices[namespaceName]?.label;
4802
- if (label && label !== appName) resourceOwners.add(label);
4803
- if (label === appName) changeSet.deletes.push({
5049
+ const entry = existingServices[namespaceName];
5050
+ const label = entry?.label;
5051
+ const owned = isOwnedByApp(entry?.allLabels, appName, appId);
5052
+ if (label && !owned) resourceOwners.add(label);
5053
+ if (owned) changeSet.deletes.push({
4804
5054
  name: namespaceName,
4805
5055
  request: {
4806
5056
  workspaceId,
@@ -5062,17 +5312,22 @@ async function planSecretManager(context) {
5062
5312
  const vaultName = vault.vaultName;
5063
5313
  const existing = existingVaults[vaultName];
5064
5314
  if (existing) {
5065
- const metaRequest = await buildMetaRequest(vaultTrn$1(workspaceId, vaultName), application.name);
5066
- if (!existing.label) unmanaged.push({
5315
+ const metaRequest = await buildMetaRequest({
5316
+ trn: vaultTrn$1(workspaceId, vaultName),
5317
+ appName: application.name,
5318
+ appId: application.id
5319
+ });
5320
+ const owned = isOwnedByApp(existing.allLabels, application.name, application.id);
5321
+ if (!owned) if (!existing.label) unmanaged.push({
5067
5322
  resourceType: "Secret Manager vault",
5068
5323
  resourceName: vaultName
5069
5324
  });
5070
- else if (existing.label !== application.name) conflicts.push({
5325
+ else conflicts.push({
5071
5326
  resourceType: "Secret Manager vault",
5072
5327
  resourceName: vaultName,
5073
5328
  currentOwner: existing.label
5074
5329
  });
5075
- if (existing.label === application.name && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) vaultChangeSet.unchanged.push({ name: vaultName });
5330
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels)) vaultChangeSet.unchanged.push({ name: vaultName });
5076
5331
  else vaultChangeSet.updates.push({
5077
5332
  name: vaultName,
5078
5333
  workspaceId
@@ -5133,8 +5388,9 @@ async function planSecretManager(context) {
5133
5388
  for (const [name, entry] of Object.entries(existingVaults)) {
5134
5389
  if (!entry) continue;
5135
5390
  const label = entry.label;
5136
- if (label && label !== application.name) resourceOwners.add(label);
5137
- if (label === application.name) {
5391
+ const owned = isOwnedByApp(entry.allLabels, application.name, application.id);
5392
+ if (label && !owned) resourceOwners.add(label);
5393
+ if (owned) {
5138
5394
  const secrets = await fetchAll(async (pageToken, maxPageSize) => {
5139
5395
  try {
5140
5396
  const { secrets, nextPageToken } = await client.listSecretManagerSecrets({
@@ -5190,12 +5446,20 @@ async function applySecretManager(client, result, phase = "create-update", appli
5190
5446
  secretmanagerVaultName: create.name
5191
5447
  });
5192
5448
  if (application) {
5193
- const metaRequest = await buildMetaRequest(vaultTrn$1(create.workspaceId, create.name), application.name);
5449
+ const metaRequest = await buildMetaRequest({
5450
+ trn: vaultTrn$1(create.workspaceId, create.name),
5451
+ appName: application.name,
5452
+ appId: application.id
5453
+ });
5194
5454
  await client.setMetadata(metaRequest);
5195
5455
  }
5196
5456
  }));
5197
5457
  if (application) await Promise.all(vaultChangeSet.updates.map(async (update) => {
5198
- const metaRequest = await buildMetaRequest(vaultTrn$1(update.workspaceId, update.name), application.name);
5458
+ const metaRequest = await buildMetaRequest({
5459
+ trn: vaultTrn$1(update.workspaceId, update.name),
5460
+ appName: application.name,
5461
+ appId: application.id
5462
+ });
5199
5463
  await client.setMetadata(metaRequest);
5200
5464
  }));
5201
5465
  await Promise.all(secretChangeSet.creates.map((create) => client.createSecretManagerSecret({
@@ -5316,7 +5580,11 @@ async function planStaticWebsite(context) {
5316
5580
  const config = websiteService;
5317
5581
  const name = websiteService.name;
5318
5582
  const existing = existingWebsites[name];
5319
- const metaRequest = await buildMetaRequest(trn$1(workspaceId, name), application.name);
5583
+ const metaRequest = await buildMetaRequest({
5584
+ trn: trn$1(workspaceId, name),
5585
+ appName: application.name,
5586
+ appId: application.id
5587
+ });
5320
5588
  const desired = normalizeComparableStaticWebsite(config);
5321
5589
  const request = {
5322
5590
  workspaceId,
@@ -5327,17 +5595,17 @@ async function planStaticWebsite(context) {
5327
5595
  }
5328
5596
  };
5329
5597
  if (existing) {
5330
- const isManagedByApp = existing.label === application.name;
5331
- if (!existing.label) unmanaged.push({
5598
+ const owned = isOwnedByApp(existing.allLabels, application.name, application.id);
5599
+ if (!owned) if (!existing.label) unmanaged.push({
5332
5600
  resourceType: "StaticWebsite",
5333
5601
  resourceName: name
5334
5602
  });
5335
- else if (existing.label !== application.name) conflicts.push({
5603
+ else conflicts.push({
5336
5604
  resourceType: "StaticWebsite",
5337
5605
  resourceName: name,
5338
5606
  currentOwner: existing.label
5339
5607
  });
5340
- if (isManagedByApp && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areStaticWebsitesEqual(existing.resource, desired)) changeSet.unchanged.push({ name });
5608
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areStaticWebsitesEqual(existing.resource, desired)) changeSet.unchanged.push({ name });
5341
5609
  else changeSet.updates.push({
5342
5610
  name,
5343
5611
  request,
@@ -5351,9 +5619,11 @@ async function planStaticWebsite(context) {
5351
5619
  });
5352
5620
  }
5353
5621
  Object.entries(existingWebsites).forEach(([name]) => {
5354
- const label = existingWebsites[name]?.label;
5355
- if (label && label !== application.name) resourceOwners.add(label);
5356
- if (label === application.name) changeSet.deletes.push({
5622
+ const entry = existingWebsites[name];
5623
+ const label = entry?.label;
5624
+ const owned = isOwnedByApp(entry?.allLabels, application.name, application.id);
5625
+ if (label && !owned) resourceOwners.add(label);
5626
+ if (owned) changeSet.deletes.push({
5357
5627
  name,
5358
5628
  request: {
5359
5629
  workspaceId,
@@ -7286,7 +7556,7 @@ function formatRemoteVerificationResults(results) {
7286
7556
  * @param {ReadonlyMap<string, Record<string, TailorDBType>>} typesByNamespace - Types by namespace
7287
7557
  * @param {LoadedConfig} config - Loaded application config (includes path)
7288
7558
  * @param {boolean} noSchemaCheck - Whether to skip schema diff check
7289
- * @returns {Promise<PendingMigration[]>} List of pending migrations
7559
+ * @returns {Promise<ValidateAndDetectResult>} Pending migrations and namespaces that have migration directories configured
7290
7560
  */
7291
7561
  async function validateAndDetectMigrations(client, workspaceId, typesByNamespace, config, noSchemaCheck) {
7292
7562
  const namespacesWithMigrations = getNamespacesWithMigrations(config, path.dirname(config.path));
@@ -7327,7 +7597,38 @@ async function validateAndDetectMigrations(client, workspaceId, typesByNamespace
7327
7597
  if (withScripts.length > 0) logger.info(` • ${withScripts.length} data migration(s) (requires migration script execution)`, { mode: "plain" });
7328
7598
  }
7329
7599
  }
7330
- return pendingMigrations;
7600
+ return {
7601
+ pendingMigrations,
7602
+ namespacesWithMigrations
7603
+ };
7604
+ }
7605
+ /**
7606
+ * Reconcile the on-remote migration label with the working tree's latest
7607
+ * migration number for each namespace.
7608
+ *
7609
+ * Used after a `--no-schema-check` apply: that flag skips the local/remote
7610
+ * snapshot drift checks, but if it also leaves the label untouched the remote
7611
+ * label can drift past the working tree's latest migration (e.g. when
7612
+ * checking out an older revision and re-deploying). A subsequent run would
7613
+ * then reconstruct the expected snapshot at a label that no longer exists in
7614
+ * the working tree, triggering a false drift error.
7615
+ *
7616
+ * Always force `label = working_tree_max` regardless of the previous label so
7617
+ * the invariant `label <= working_tree_max` is preserved.
7618
+ * @param client - Operator client instance
7619
+ * @param workspaceId - Workspace ID
7620
+ * @param namespacesWithMigrations - Namespaces that have migration directories configured
7621
+ */
7622
+ async function reconcileMigrationLabels(client, workspaceId, namespacesWithMigrations) {
7623
+ for (const { namespace, migrationsDir } of namespacesWithMigrations) {
7624
+ const targetVersion = getLatestMigrationNumber(migrationsDir);
7625
+ const currentVersion = await getRemoteMigrationNumber(client, workspaceId, namespace);
7626
+ await updateMigrationLabel(client, workspaceId, namespace, targetVersion);
7627
+ if (currentVersion !== targetVersion) {
7628
+ const from = currentVersion === null ? "<unset>" : formatMigrationNumber(currentVersion);
7629
+ logger.info(`Migration label for namespace ${namespace} reconciled: ${from} → ${formatMigrationNumber(targetVersion)}.`);
7630
+ }
7631
+ }
7331
7632
  }
7332
7633
  /**
7333
7634
  * Build migration execution context for script-based migrations.
@@ -7363,7 +7664,7 @@ async function applyTailorDB(client, result, phase = "create-update") {
7363
7664
  const types = tailordb.types;
7364
7665
  if (types) typesByNamespace.set(tailordb.namespace, types);
7365
7666
  }
7366
- const pendingMigrations = await validateAndDetectMigrations(client, migrationContext.workspaceId, typesByNamespace, migrationContext.config, migrationContext.noSchemaCheck);
7667
+ const { pendingMigrations, namespacesWithMigrations } = await validateAndDetectMigrations(client, migrationContext.workspaceId, typesByNamespace, migrationContext.config, migrationContext.noSchemaCheck);
7367
7668
  if (pendingMigrations.length > 0) {
7368
7669
  processedTypes.reset();
7369
7670
  deletedResources.reset();
@@ -7403,6 +7704,7 @@ async function applyTailorDB(client, result, phase = "create-update") {
7403
7704
  await Promise.all(changeSet.gqlPermission.deletes.map((del) => client.deleteTailorDBGQLPermission(del.request)));
7404
7705
  await Promise.all(changeSet.type.deletes.map((del) => client.deleteTailorDBType(del.request)));
7405
7706
  }
7707
+ if (migrationContext.noSchemaCheck && namespacesWithMigrations.length > 0) await reconcileMigrationLabels(client, migrationContext.workspaceId, namespacesWithMigrations);
7406
7708
  } else if (phase === "delete-resources") {
7407
7709
  await Promise.all(changeSet.gqlPermission.deletes.map((del) => client.deleteTailorDBGQLPermission(del.request)));
7408
7710
  await Promise.all(changeSet.type.deletes.map((del) => client.deleteTailorDBType(del.request)));
@@ -7657,7 +7959,7 @@ async function planTailorDB(context) {
7657
7959
  tailordbs.push(tailordb);
7658
7960
  }
7659
7961
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
7660
- const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, tailordbs);
7962
+ const { changeSet: serviceChangeSet, conflicts, unmanaged, resourceOwners } = await planServices(client, workspaceId, application.name, application.id, tailordbs);
7661
7963
  const deletedServices = serviceChangeSet.deletes.map((del) => del.name);
7662
7964
  const [typeChangeSet, gqlPermissionChangeSet] = await Promise.all([planTypes(client, workspaceId, tailordbs, executors, deletedServices, void 0, forceApplyAll), planGqlPermissions(client, workspaceId, tailordbs, deletedServices, forceApplyAll)]);
7663
7965
  return {
@@ -7731,7 +8033,7 @@ function areTailorDBServicesEqual(existing, desired) {
7731
8033
  defaultTimezone: "UTC"
7732
8034
  }));
7733
8035
  }
7734
- async function planServices(client, workspaceId, appName, tailordbs) {
8036
+ async function planServices(client, workspaceId, appName, appId, tailordbs) {
7735
8037
  const changeSet = createChangeSet("TailorDB services");
7736
8038
  const conflicts = [];
7737
8039
  const unmanaged = [];
@@ -7761,18 +8063,24 @@ async function planServices(client, workspaceId, appName, tailordbs) {
7761
8063
  }));
7762
8064
  for (const tailordb of tailordbs) {
7763
8065
  const existing = existingServices[tailordb.namespace];
7764
- const metaRequest = await buildMetaRequest(trn(workspaceId, tailordb.namespace), appName, existing?.allLabels);
8066
+ const metaRequest = await buildMetaRequest({
8067
+ trn: trn(workspaceId, tailordb.namespace),
8068
+ appName,
8069
+ appId,
8070
+ existingLabels: existing?.allLabels
8071
+ });
7765
8072
  if (existing) {
7766
- if (!existing.label) unmanaged.push({
8073
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
8074
+ if (!owned) if (!existing.label) unmanaged.push({
7767
8075
  resourceType: "TailorDB service",
7768
8076
  resourceName: tailordb.namespace
7769
8077
  });
7770
- else if (existing.label !== appName) conflicts.push({
8078
+ else conflicts.push({
7771
8079
  resourceType: "TailorDB service",
7772
8080
  resourceName: tailordb.namespace,
7773
8081
  currentOwner: existing.label
7774
8082
  });
7775
- if (existing.label === appName && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areTailorDBServicesEqual(existing.resource, tailordb)) changeSet.unchanged.push({ name: tailordb.namespace });
8083
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && areTailorDBServicesEqual(existing.resource, tailordb)) changeSet.unchanged.push({ name: tailordb.namespace });
7776
8084
  else changeSet.updates.push({
7777
8085
  name: tailordb.namespace,
7778
8086
  metaRequest
@@ -7789,9 +8097,11 @@ async function planServices(client, workspaceId, appName, tailordbs) {
7789
8097
  });
7790
8098
  }
7791
8099
  Object.entries(existingServices).forEach(([namespaceName]) => {
7792
- const label = existingServices[namespaceName]?.label;
7793
- if (label && label !== appName) resourceOwners.add(label);
7794
- if (label === appName) changeSet.deletes.push({
8100
+ const entry = existingServices[namespaceName];
8101
+ const label = entry?.label;
8102
+ const owned = isOwnedByApp(entry?.allLabels, appName, appId);
8103
+ if (label && !owned) resourceOwners.add(label);
8104
+ if (owned) changeSet.deletes.push({
7795
8105
  name: namespaceName,
7796
8106
  request: {
7797
8107
  workspaceId,
@@ -8434,9 +8744,9 @@ function formatMigrationCheckResults(results) {
8434
8744
  * @returns Promise that resolves when workflows are applied
8435
8745
  */
8436
8746
  async function applyWorkflow(client, result, phase = "create-update") {
8437
- const { changeSet, appName } = result;
8747
+ const { changeSet, appName, appId } = result;
8438
8748
  if (phase === "create-update") {
8439
- const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, result.unchangedWorkflowJobNames);
8749
+ const jobFunctionVersions = await registerJobFunctions(client, changeSet, appName, appId, result.unchangedWorkflowJobNames);
8440
8750
  await Promise.all([...changeSet.creates.map(async (create) => {
8441
8751
  const filteredVersions = filterJobFunctionVersions(jobFunctionVersions, create.usedJobNames);
8442
8752
  await client.createWorkflow({
@@ -8484,10 +8794,11 @@ function filterJobFunctionVersions(allVersions, usedJobNames) {
8484
8794
  * @param client - Operator client instance
8485
8795
  * @param changeSet - Workflow change set
8486
8796
  * @param appName - Application name
8797
+ * @param appId
8487
8798
  * @param unchangedWorkflowJobNames - Job function names used by unchanged workflows
8488
8799
  * @returns Map of job function names to versions
8489
8800
  */
8490
- async function registerJobFunctions(client, changeSet, appName, unchangedWorkflowJobNames = /* @__PURE__ */ new Set()) {
8801
+ async function registerJobFunctions(client, changeSet, appName, appId, unchangedWorkflowJobNames = /* @__PURE__ */ new Set()) {
8491
8802
  const jobFunctionVersions = {};
8492
8803
  const firstWorkflow = changeSet.creates[0] || changeSet.updates[0] || changeSet.deletes[0];
8493
8804
  if (!firstWorkflow) return jobFunctionVersions;
@@ -8515,7 +8826,11 @@ async function registerJobFunctions(client, changeSet, appName, unchangedWorkflo
8515
8826
  jobFunctionName: jobName,
8516
8827
  scriptRef: workflowJobFunctionName(jobName)
8517
8828
  });
8518
- await client.setMetadata(await buildMetaRequest(jobFunctionTrn(workspaceId, jobName), appName));
8829
+ await client.setMetadata(await buildMetaRequest({
8830
+ trn: jobFunctionTrn(workspaceId, jobName),
8831
+ appName,
8832
+ appId
8833
+ }));
8519
8834
  return {
8520
8835
  jobName,
8521
8836
  version: response.jobFunction?.version
@@ -8526,7 +8841,7 @@ async function registerJobFunctions(client, changeSet, appName, unchangedWorkflo
8526
8841
  const unusedJobFunctions = existingJobFunctions.filter((jobName) => !allUsedJobNames.has(jobName));
8527
8842
  await Promise.all(unusedJobFunctions.map(async (jobName) => {
8528
8843
  const { metadata } = await client.getMetadata({ trn: jobFunctionTrn(workspaceId, jobName) });
8529
- if (metadata?.labels?.["sdk-name"] === appName) await client.setMetadata({
8844
+ if (isOwnedByApp(metadata?.labels, appName, appId)) await client.setMetadata({
8530
8845
  trn: jobFunctionTrn(workspaceId, jobName),
8531
8846
  labels: { [sdkNameLabelKey]: "" }
8532
8847
  });
@@ -8564,12 +8879,13 @@ function jobFunctionTrn(workspaceId, name) {
8564
8879
  * @param client - Operator client instance
8565
8880
  * @param workspaceId - Workspace ID
8566
8881
  * @param appName - Application name
8882
+ * @param appId
8567
8883
  * @param workflows - Parsed workflows
8568
8884
  * @param mainJobDeps - Main job dependencies by workflow
8569
8885
  * @param unchangedJobFunctions - Job functions already proven unchanged by function registry plan
8570
8886
  * @returns Planned workflow changes
8571
8887
  */
8572
- async function planWorkflow(client, workspaceId, appName, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set()) {
8888
+ async function planWorkflow(client, workspaceId, appName, appId, workflows, mainJobDeps, unchangedJobFunctions = /* @__PURE__ */ new Set()) {
8573
8889
  const changeSet = createChangeSet("Workflows");
8574
8890
  const conflicts = [];
8575
8891
  const unmanaged = [];
@@ -8594,20 +8910,25 @@ async function planWorkflow(client, workspaceId, appName, workflows, mainJobDeps
8594
8910
  }));
8595
8911
  for (const workflow of Object.values(workflows)) {
8596
8912
  const existing = existingWorkflows[workflow.name];
8597
- const metaRequest = await buildMetaRequest(workflowTrn$1(workspaceId, workflow.name), appName);
8913
+ const metaRequest = await buildMetaRequest({
8914
+ trn: workflowTrn$1(workspaceId, workflow.name),
8915
+ appName,
8916
+ appId
8917
+ });
8598
8918
  const usedJobNames = mainJobDeps[workflow.mainJob.name];
8599
8919
  if (!usedJobNames) throw new Error(`Job "${workflow.mainJob.name}" (mainJob of workflow "${workflow.name}") was not found.\n\nPossible causes:\n - The job is not exported as a named export\n - The file containing the job is not included in workflow.files glob pattern\n\nSolution:\n export const ${workflow.mainJob.name} = createWorkflowJob({ name: "${workflow.mainJob.name}", ... })`);
8600
8920
  if (existing) {
8601
- if (!existing.label) unmanaged.push({
8921
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
8922
+ if (!owned) if (!existing.label) unmanaged.push({
8602
8923
  resourceType: "Workflow",
8603
8924
  resourceName: workflow.name
8604
8925
  });
8605
- else if (existing.label !== appName) conflicts.push({
8926
+ else conflicts.push({
8606
8927
  resourceType: "Workflow",
8607
8928
  resourceName: workflow.name,
8608
8929
  currentOwner: existing.label
8609
8930
  });
8610
- if (existing.label === appName && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged(existing.resource, workflow, usedJobNames, unchangedJobFunctions)) {
8931
+ if (owned && hasMatchingSdkVersion(existing.allLabels, metaRequest.labels) && canTreatWorkflowAsUnchanged(existing.resource, workflow, usedJobNames, unchangedJobFunctions)) {
8611
8932
  changeSet.unchanged.push({ name: workflow.name });
8612
8933
  for (const jobName of usedJobNames) unchangedWorkflowJobNames.add(jobName);
8613
8934
  } else changeSet.updates.push({
@@ -8629,8 +8950,9 @@ async function planWorkflow(client, workspaceId, appName, workflows, mainJobDeps
8629
8950
  Object.values(existingWorkflows).forEach((existing) => {
8630
8951
  if (!existing) return;
8631
8952
  const label = existing.label;
8632
- if (label && label !== appName) resourceOwners.add(label);
8633
- if (label === appName) changeSet.deletes.push({
8953
+ const owned = isOwnedByApp(existing.allLabels, appName, appId);
8954
+ if (label && !owned) resourceOwners.add(label);
8955
+ if (owned) changeSet.deletes.push({
8634
8956
  name: existing.resource.name,
8635
8957
  workspaceId,
8636
8958
  workflowId: existing.resource.id,
@@ -8643,6 +8965,7 @@ async function planWorkflow(client, workspaceId, appName, workflows, mainJobDeps
8643
8965
  unmanaged,
8644
8966
  resourceOwners,
8645
8967
  appName,
8968
+ appId,
8646
8969
  unchangedWorkflowJobNames
8647
8970
  };
8648
8971
  }
@@ -8765,7 +9088,11 @@ function collectIdpUserTriggerTargets(application) {
8765
9088
  return targets;
8766
9089
  }
8767
9090
  async function shouldForceApplyAll(client, workspaceId, application, functionEntries) {
8768
- const desiredLabels = (await buildMetaRequest(applicationTrn(workspaceId, application.name), application.name)).labels;
9091
+ const desiredLabels = (await buildMetaRequest({
9092
+ trn: applicationTrn(workspaceId, application.name),
9093
+ appName: application.name,
9094
+ appId: application.id
9095
+ })).labels;
8769
9096
  const candidateTrns = /* @__PURE__ */ new Set();
8770
9097
  if (application.subgraphs.length > 0) candidateTrns.add(applicationTrn(workspaceId, application.name));
8771
9098
  application.staticWebsiteServices.forEach((website) => {
@@ -8902,9 +9229,16 @@ async function deploy(options) {
8902
9229
  return withSpan("deploy", async (rootSpan) => {
8903
9230
  rootSpan.setAttribute("deploy.dry_run", options?.dryRun ?? false);
8904
9231
  const { config, application, workflowBuildResult, bundledScripts, buildOnly } = await withSpan("build", async () => {
8905
- const { config, plugins } = await withSpan("build.loadConfig", () => loadConfig(options?.configPath));
8906
9232
  const dryRun = options?.dryRun ?? false;
8907
9233
  const buildOnly = options?.buildOnly ?? parseBoolean(process.env.TAILOR_PLATFORM_SDK_BUILD_ONLY) === true;
9234
+ const { config, plugins } = await withSpan("build.loadConfig", async () => {
9235
+ const foundPath = loadConfigPath(options?.configPath);
9236
+ if (foundPath && !dryRun && !buildOnly) {
9237
+ const resolvedPath = path.resolve(process.cwd(), foundPath);
9238
+ if (fs$1.existsSync(resolvedPath)) await ensureConfigId(resolvedPath);
9239
+ }
9240
+ return loadConfig(options?.configPath);
9241
+ });
8908
9242
  const noCache = options?.noCache ?? false;
8909
9243
  const packageJson = await readPackageJson();
8910
9244
  const cacheDir = path.resolve(getDistDir(), "cache");
@@ -8982,7 +9316,7 @@ async function deploy(options) {
8982
9316
  forceApplyAll,
8983
9317
  idpUserTriggerTargets
8984
9318
  };
8985
- const functionRegistry = await withSpan("plan.functionRegistry", () => planFunctionRegistry(client, workspaceId, application.name, functionEntries));
9319
+ const functionRegistry = await withSpan("plan.functionRegistry", () => planFunctionRegistry(client, workspaceId, application.name, application.id, functionEntries));
8986
9320
  const unchangedWorkflowJobs = new Set(functionRegistry.changeSet.unchanged.filter((entry) => entry.name.startsWith(WORKFLOW_PREFIX)).map((entry) => entry.name.slice(WORKFLOW_PREFIX.length)));
8987
9321
  const [tailorDB, staticWebsite, idp, auth, pipeline, app, executor, workflow, secretManager] = await Promise.all([
8988
9322
  withSpan("plan.tailorDB", () => planTailorDB(ctx)),
@@ -8992,7 +9326,7 @@ async function deploy(options) {
8992
9326
  withSpan("plan.pipeline", () => planPipeline(ctx)),
8993
9327
  withSpan("plan.application", () => planApplication(ctx)),
8994
9328
  withSpan("plan.executor", () => planExecutor(ctx)),
8995
- withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
9329
+ withSpan("plan.workflow", () => planWorkflow(client, workspaceId, application.name, application.id, workflowService?.workflows ?? {}, workflowBuildResult?.mainJobDeps ?? {}, unchangedWorkflowJobs)),
8996
9330
  withSpan("plan.secretManager", () => planSecretManager(ctx))
8997
9331
  ]);
8998
9332
  return {
@@ -10688,6 +11022,7 @@ The \`--logs\` option displays logs from the downstream execution when available
10688
11022
  })
10689
11023
  }).strict(),
10690
11024
  run: async (args) => {
11025
+ await assertWritable({ profile: args.profile });
10691
11026
  const client = await initOperatorClient(await loadAccessToken({
10692
11027
  useProfile: true,
10693
11028
  profile: args.profile
@@ -11820,7 +12155,9 @@ function createGenerationManager(params) {
11820
12155
  */
11821
12156
  async function generate$1(options) {
11822
12157
  return withSpan("generate", async (rootSpan) => {
11823
- const { config, generators, plugins } = await withSpan("generate.loadConfig", async () => loadConfig(options?.configPath));
12158
+ const { config, generators, plugins } = await withSpan("generate.loadConfig", async () => {
12159
+ return loadConfig(options?.configPath);
12160
+ });
11824
12161
  const watch = options?.watch ?? false;
11825
12162
  rootSpan.setAttribute("generate.watch", watch);
11826
12163
  rootSpan.setAttribute("generate.generators.count", generators.length);
@@ -12192,6 +12529,7 @@ const createCommand$1 = defineAppCommand({
12192
12529
  })
12193
12530
  }).strict(),
12194
12531
  run: async (args) => {
12532
+ await assertWritable();
12195
12533
  const folder = await createFolder({
12196
12534
  organizationId: args["organization-id"],
12197
12535
  parentFolderId: args["parent-folder-id"],
@@ -12230,6 +12568,7 @@ const deleteCommand$1 = defineAppCommand({
12230
12568
  ...confirmationArgs
12231
12569
  }).strict(),
12232
12570
  run: async (args) => {
12571
+ await assertWritable();
12233
12572
  const client = await initOperatorClient(await loadAccessToken());
12234
12573
  let folderName;
12235
12574
  try {
@@ -12380,6 +12719,7 @@ const updateCommand$2 = defineAppCommand({
12380
12719
  })
12381
12720
  }).strict(),
12382
12721
  run: async (args) => {
12722
+ await assertWritable();
12383
12723
  const folder = await updateFolder({
12384
12724
  organizationId: args["organization-id"],
12385
12725
  folderId: args["folder-id"],
@@ -12605,6 +12945,7 @@ const updateCommand$1 = defineAppCommand({
12605
12945
  })
12606
12946
  }).strict(),
12607
12947
  run: async (args) => {
12948
+ await assertWritable();
12608
12949
  const organization = await updateOrganization({
12609
12950
  organizationId: args["organization-id"],
12610
12951
  name: args.name
@@ -12648,8 +12989,8 @@ async function execRemove(client, workspaceId, application, config, confirm) {
12648
12989
  const pipeline = await planPipeline(ctx);
12649
12990
  const app = await planApplication(ctx);
12650
12991
  const executor = await planExecutor(ctx);
12651
- const workflow = await planWorkflow(client, workspaceId, application.name, {}, {});
12652
- const functionRegistry = await planFunctionRegistry(client, workspaceId, application.name, []);
12992
+ const workflow = await planWorkflow(client, workspaceId, application.name, application.id, {}, {});
12993
+ const functionRegistry = await planFunctionRegistry(client, workspaceId, application.name, application.id, []);
12653
12994
  const secretManager = await planSecretManager(ctx);
12654
12995
  functionRegistry.changeSet.print();
12655
12996
  staticWebsite.changeSet.print();
@@ -12709,6 +13050,7 @@ const removeCommand$1 = defineAppCommand({
12709
13050
  ...confirmationArgs
12710
13051
  }).strict(),
12711
13052
  run: async (args) => {
13053
+ await assertWritable({ profile: args.profile });
12712
13054
  const { client, workspaceId, application, config } = await loadOptions$10({
12713
13055
  workspaceId: args["workspace-id"],
12714
13056
  profile: args.profile,
@@ -13372,7 +13714,7 @@ async function generate(options) {
13372
13714
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
13373
13715
  let pluginManager;
13374
13716
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
13375
- const { defineApplication } = await import("./application-Csq5jxYP.mjs");
13717
+ const { defineApplication } = await import("./application-v_E2W-Fz.mjs");
13376
13718
  const application = defineApplication({
13377
13719
  config,
13378
13720
  pluginManager
@@ -13702,6 +14044,7 @@ const truncateCommand = defineAppCommand({
13702
14044
  })
13703
14045
  }).strict(),
13704
14046
  run: async (args) => {
14047
+ await assertWritable({ profile: args.profile });
13705
14048
  const types = args.types && args.types.length > 0 ? args.types : void 0;
13706
14049
  await $truncate({
13707
14050
  workspaceId: args["workspace-id"],
@@ -14084,9 +14427,11 @@ const createCommand = defineAppCommand({
14084
14427
  alias: "p",
14085
14428
  description: "Profile name to create"
14086
14429
  }),
14087
- "profile-user": arg(z.string().optional(), { description: "User email for the profile (defaults to current user)" })
14430
+ "profile-user": arg(z.string().optional(), { description: "User email for the profile (defaults to current user)" }),
14431
+ permission: arg(z.enum(["write", "read"]).default("write"), { description: "Profile permission (requires --profile-name). 'read' blocks all write commands while the profile is active." })
14088
14432
  }).strict(),
14089
14433
  run: async (args) => {
14434
+ await assertWritable();
14090
14435
  const workspace = await createWorkspace({
14091
14436
  name: args.name,
14092
14437
  region: args.region,
@@ -14104,13 +14449,15 @@ const createCommand = defineAppCommand({
14104
14449
  if (!config.users[profileUser]) throw new Error(`User "${profileUser}" not found.\nPlease verify your user name and login using 'tailor-sdk login' command.`);
14105
14450
  config.profiles[profileName] = {
14106
14451
  user: profileUser,
14107
- workspace_id: workspace.id
14452
+ workspace_id: workspace.id,
14453
+ ...args.permission === "read" ? { readonly: true } : {}
14108
14454
  };
14109
14455
  writePlatformConfig(config);
14110
14456
  profileInfo = {
14111
14457
  name: profileName,
14112
14458
  user: profileUser,
14113
- workspaceId: workspace.id
14459
+ workspaceId: workspace.id,
14460
+ permission: args.permission
14114
14461
  };
14115
14462
  if (!args.json) logger.success(`Profile "${profileName}" created successfully.`);
14116
14463
  }
@@ -14161,6 +14508,7 @@ const deleteCommand = defineAppCommand({
14161
14508
  ...confirmationArgs
14162
14509
  }).strict(),
14163
14510
  run: async (args) => {
14511
+ await assertWritable();
14164
14512
  const { client, workspaceId } = await loadOptions$7({ workspaceId: args["workspace-id"] });
14165
14513
  let workspace;
14166
14514
  try {
@@ -14298,6 +14646,7 @@ const restoreCommand = defineAppCommand({
14298
14646
  ...confirmationArgs
14299
14647
  }).strict(),
14300
14648
  run: async (args) => {
14649
+ await assertWritable();
14301
14650
  const { client, workspaceId } = await loadOptions$5({ workspaceId: args["workspace-id"] });
14302
14651
  if (!args.yes) {
14303
14652
  if (await prompt.text({ message: `Are you sure you want to restore workspace "${workspaceId}"? (yes/no):` }) !== "yes") {
@@ -14390,6 +14739,7 @@ const inviteCommand = defineAppCommand({
14390
14739
  })
14391
14740
  }).strict(),
14392
14741
  run: async (args) => {
14742
+ await assertWritable({ profile: args.profile });
14393
14743
  await inviteUser({
14394
14744
  workspaceId: args["workspace-id"],
14395
14745
  profile: args.profile,
@@ -14503,6 +14853,7 @@ const removeCommand = defineAppCommand({
14503
14853
  ...confirmationArgs
14504
14854
  }).strict(),
14505
14855
  run: async (args) => {
14856
+ await assertWritable({ profile: args.profile });
14506
14857
  if (!args.yes) {
14507
14858
  if (await prompt.text({ message: `Are you sure you want to remove user "${args.email}" from the workspace? (yes/no):` }) !== "yes") {
14508
14859
  logger.info("User removal cancelled.");
@@ -14567,6 +14918,7 @@ const updateCommand = defineAppCommand({
14567
14918
  })
14568
14919
  }).strict(),
14569
14920
  run: async (args) => {
14921
+ await assertWritable({ profile: args.profile });
14570
14922
  await updateUser({
14571
14923
  workspaceId: args["workspace-id"],
14572
14924
  profile: args.profile,
@@ -15187,7 +15539,7 @@ async function runRepl(options) {
15187
15539
  const execute = await prepareQueryExecutor(options);
15188
15540
  const historyPath = getReplHistoryPath(options.engine, options.profile, options.workspaceId);
15189
15541
  const validate = createReplValidator(options.engine);
15190
- const { highlightSqlLine, highlightGraphqlLine, replTransform } = await import("./repl-editor-CZpLlOBj.mjs");
15542
+ const { highlightSqlLine, highlightGraphqlLine, replTransform } = await import("./repl-editor-jZ493eQI.mjs");
15191
15543
  const highlight = options.engine === "sql" ? highlightSqlLine : highlightGraphqlLine;
15192
15544
  const prompt = createPrompt({
15193
15545
  prefix: "",
@@ -15520,5 +15872,5 @@ function isDeno() {
15520
15872
  }
15521
15873
 
15522
15874
  //#endregion
15523
- export { deleteCommand$1 as $, getMigrationDirPath as $t, truncate as A, executionsCommand as At, updateOrganization as B, MIGRATION_LABEL_KEY as Bt, listCommand$2 as C, toPageDirection as Cn, jobsCommand as Ct, resumeWorkflow as D, startWorkflow as Dt, resumeCommand as E, startCommand as Et, showCommand as F, getCommand$6 as Ft, getCommand$1 as G, INITIAL_SCHEMA_NUMBER as Gt, treeCommand as H, bundleMigrationScript as Ht, logBetaWarning as I, getExecutor as It, updateFolder as J, compareLocalTypesWithSnapshot as Jt, getOrganization as K, MIGRATE_FILE_NAME as Kt, remove as L, deploy as Lt, generate as M, listWorkflowExecutions as Mt, generateCommand as N, functionExecutionStatusToString as Nt, listCommand$3 as O, getCommand$5 as Ot, show as P, formatKeyValueTable as Pt, getFolder as Q, getLatestMigrationNumber as Qt, removeCommand$1 as R, executeScript as Rt, listApps as S, paginationArgs as Sn, getExecutorJob as St, healthCommand as T, watchExecutorJob as Tt, listCommand$4 as U, DB_TYPES_FILE_NAME as Ut, organizationTree as V, parseMigrationLabelNumber as Vt, listOrganizations as W, DIFF_FILE_NAME as Wt, listFolders as X, createSnapshotFromLocalTypes as Xt, listCommand$5 as Y, compareSnapshots as Yt, getCommand$2 as Z, formatMigrationNumber as Zt, getWorkspace as _, commonArgs as _n, webhookCommand as _t, updateUser as a, reconstructSnapshotFromMigrations as an, getCommand$3 as at, createCommand as b, isVerbose as bn, listCommand$9 as bt, listCommand as c, hasChanges as cn, tokenCommand as ct, inviteUser as d, trnPrefix as dn, generate$1 as dt, getMigrationFilePath as en, deleteFolder as et, restoreCommand as f, generateUserTypes as fn, listCommand$8 as ft, getCommand as g, defineAppCommand as gn, listWebhookExecutors as gt, listWorkspaces as h, apiCall as hn, getFunctionRegistry as ht, updateCommand as i, loadDiff as in, listOAuth2Clients as it, truncateCommand as j, getWorkflowExecution as jt, listWorkflows as k, getWorkflow as kt, listUsers as l, getNamespacesWithMigrations as ln, listCommand$7 as lt, listCommand$1 as m, apiCommand as mn, getCommand$4 as mt, query as n, getNextMigrationNumber as nn, createFolder as nt, removeCommand as o, formatDiffSummary as on, getOAuth2Client as ot, restoreWorkspace as p, prompt as pn, listFunctionRegistries as pt, updateCommand$2 as q, SCHEMA_FILE_NAME as qt, queryCommand as r, isValidMigrationNumber as rn, listCommand$6 as rt, removeUser as s, formatMigrationDiff as sn, getMachineUserToken as st, isNativeTypeScriptRuntime as t, getMigrationFiles as tn, createCommand$1 as tt, inviteCommand as u, sdkNameLabelKey as un, listMachineUsers as ut, deleteCommand as v, confirmationArgs as vn, triggerCommand as vt, getAppHealth as w, workspaceArgs as wn, listExecutorJobs as wt, createWorkspace as x, pagedLogArgs as xn, listExecutors as xt, deleteWorkspace as y, deploymentArgs as yn, triggerExecutor as yt, updateCommand$1 as z, waitForExecution$1 as zt };
15524
- //# sourceMappingURL=runtime-XjP6JMmP.mjs.map
15875
+ export { deleteCommand$1 as $, getMigrationDirPath as $t, truncate as A, executionsCommand as At, updateOrganization as B, MIGRATION_LABEL_KEY as Bt, listCommand$2 as C, paginationArgs as Cn, jobsCommand as Ct, resumeWorkflow as D, startWorkflow as Dt, resumeCommand as E, startCommand as Et, showCommand as F, getCommand$6 as Ft, getCommand$1 as G, INITIAL_SCHEMA_NUMBER as Gt, treeCommand as H, bundleMigrationScript as Ht, logBetaWarning as I, getExecutor as It, updateFolder as J, compareLocalTypesWithSnapshot as Jt, getOrganization as K, MIGRATE_FILE_NAME as Kt, remove as L, deploy as Lt, generate as M, listWorkflowExecutions as Mt, generateCommand as N, functionExecutionStatusToString as Nt, listCommand$3 as O, getCommand$5 as Ot, show as P, formatKeyValueTable as Pt, getFolder as Q, getLatestMigrationNumber as Qt, removeCommand$1 as R, executeScript as Rt, listApps as S, pagedLogArgs as Sn, getExecutorJob as St, healthCommand as T, workspaceArgs as Tn, watchExecutorJob as Tt, listCommand$4 as U, DB_TYPES_FILE_NAME as Ut, organizationTree as V, parseMigrationLabelNumber as Vt, listOrganizations as W, DIFF_FILE_NAME as Wt, listFolders as X, createSnapshotFromLocalTypes as Xt, listCommand$5 as Y, compareSnapshots as Yt, getCommand$2 as Z, formatMigrationNumber as Zt, getWorkspace as _, defineAppCommand as _n, webhookCommand as _t, updateUser as a, reconstructSnapshotFromMigrations as an, getCommand$3 as at, createCommand as b, deploymentArgs as bn, listCommand$9 as bt, listCommand as c, hasChanges as cn, tokenCommand as ct, inviteUser as d, trnPrefix as dn, generate$1 as dt, getMigrationFilePath as en, deleteFolder as et, restoreCommand as f, generateUserTypes as fn, listCommand$8 as ft, getCommand as g, assertWritable as gn, listWebhookExecutors as gt, listWorkspaces as h, apiCall as hn, getFunctionRegistry as ht, updateCommand as i, loadDiff as in, listOAuth2Clients as it, truncateCommand as j, getWorkflowExecution as jt, listWorkflows as k, getWorkflow as kt, listUsers as l, getNamespacesWithMigrations as ln, listCommand$7 as lt, listCommand$1 as m, apiCommand as mn, getCommand$4 as mt, query as n, getNextMigrationNumber as nn, createFolder as nt, removeCommand as o, formatDiffSummary as on, getOAuth2Client as ot, restoreWorkspace as p, prompt as pn, listFunctionRegistries as pt, updateCommand$2 as q, SCHEMA_FILE_NAME as qt, queryCommand as r, isValidMigrationNumber as rn, listCommand$6 as rt, removeUser as s, formatMigrationDiff as sn, getMachineUserToken as st, isNativeTypeScriptRuntime as t, getMigrationFiles as tn, createCommand$1 as tt, inviteCommand as u, sdkNameLabelKey as un, listMachineUsers as ut, deleteCommand as v, commonArgs as vn, triggerCommand as vt, getAppHealth as w, toPageDirection as wn, listExecutorJobs as wt, createWorkspace as x, isVerbose as xn, listExecutors as xt, deleteWorkspace as y, confirmationArgs as yn, triggerExecutor as yt, updateCommand$1 as z, waitForExecution$1 as zt };
15876
+ //# sourceMappingURL=runtime-oZgK353r.mjs.map