@elevasis/sdk 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -39746,9 +39746,16 @@ var ResourceRegistry = class {
39746
39746
  }
39747
39747
  /**
39748
39748
  * Pre-serialized organization data cache
39749
- * Computed once at construction, never invalidated (static registry)
39749
+ * Computed once at construction for static orgs, updated incrementally for runtime orgs
39750
39750
  */
39751
39751
  serializedCache;
39752
+ /**
39753
+ * Per-resource remote configuration (external deployments)
39754
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
39755
+ * Tracks which individual resources were added at runtime via deploy pipeline.
39756
+ * Static and remote resources coexist in the same org.
39757
+ */
39758
+ remoteResources = /* @__PURE__ */ new Map();
39752
39759
  /**
39753
39760
  * Validates registry on construction
39754
39761
  * - Checks for duplicate resourceIds within organizations
@@ -39848,6 +39855,130 @@ var ResourceRegistry = class {
39848
39855
  return this.registry;
39849
39856
  }
39850
39857
  // ============================================================================
39858
+ // Runtime Organization Registration (External Deployments)
39859
+ // ============================================================================
39860
+ /**
39861
+ * Register external resources at runtime
39862
+ *
39863
+ * Called during deploy pipeline when an external developer deploys their bundle.
39864
+ * Merges the incoming stub definitions into the org's registry and stores
39865
+ * per-resource remote config for worker thread execution branching.
39866
+ *
39867
+ * Static and remote resources coexist in the same org. If the org already
39868
+ * has static resources, the incoming remote resources are merged alongside them.
39869
+ * If redeploying (some resources already registered as remote for this org),
39870
+ * the previous remote resources are unregistered first.
39871
+ *
39872
+ * @param orgName - Organization name (used as registry key)
39873
+ * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
39874
+ * @param remote - Remote configuration (bundle path, deployment ID, env vars)
39875
+ * @throws Error if incoming resourceId conflicts with a static resource
39876
+ * @throws Error if incoming deployment contains duplicate resourceIds
39877
+ */
39878
+ registerOrganization(orgName, org, remote) {
39879
+ const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId);
39880
+ const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId);
39881
+ const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds];
39882
+ const seen = /* @__PURE__ */ new Set();
39883
+ for (const id of incomingIds) {
39884
+ if (seen.has(id)) {
39885
+ throw new Error(
39886
+ `Duplicate resource ID '${id}' in deployment. Each resource must have a unique ID.`
39887
+ );
39888
+ }
39889
+ seen.add(id);
39890
+ }
39891
+ if (this.isRemote(orgName)) {
39892
+ this.unregisterOrganization(orgName);
39893
+ }
39894
+ const existingOrg = this.registry[orgName];
39895
+ if (existingOrg) {
39896
+ const staticWorkflowIds = new Set((existingOrg.workflows ?? []).map((w) => w.config.resourceId));
39897
+ const staticAgentIds = new Set((existingOrg.agents ?? []).map((a) => a.config.resourceId));
39898
+ for (const id of incomingIds) {
39899
+ if (staticWorkflowIds.has(id) || staticAgentIds.has(id)) {
39900
+ throw new Error(
39901
+ `Resource '${id}' already exists in '${orgName}' as an internal resource. External deployments cannot override internal resources.`
39902
+ );
39903
+ }
39904
+ }
39905
+ }
39906
+ if (existingOrg) {
39907
+ existingOrg.workflows = [...existingOrg.workflows ?? [], ...org.workflows ?? []];
39908
+ existingOrg.agents = [...existingOrg.agents ?? [], ...org.agents ?? []];
39909
+ } else {
39910
+ this.registry[orgName] = org;
39911
+ }
39912
+ for (const id of incomingIds) {
39913
+ this.remoteResources.set(`${orgName}/${id}`, remote);
39914
+ }
39915
+ this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]));
39916
+ }
39917
+ /**
39918
+ * Unregister runtime-registered resources for an organization
39919
+ *
39920
+ * Removes only resources that were registered at runtime (via registerOrganization).
39921
+ * Static resources loaded at startup are preserved. If the org still has static
39922
+ * resources after removal, the serialization cache is rebuilt. If no resources
39923
+ * remain, the org is fully removed from the registry.
39924
+ * No-op if the org has no remote resources.
39925
+ *
39926
+ * @param orgName - Organization name to unregister remote resources from
39927
+ */
39928
+ unregisterOrganization(orgName) {
39929
+ const prefix = `${orgName}/`;
39930
+ const remoteIds = /* @__PURE__ */ new Set();
39931
+ for (const key of this.remoteResources.keys()) {
39932
+ if (key.startsWith(prefix)) {
39933
+ remoteIds.add(key.slice(prefix.length));
39934
+ this.remoteResources.delete(key);
39935
+ }
39936
+ }
39937
+ if (remoteIds.size === 0) return;
39938
+ const orgResources = this.registry[orgName];
39939
+ if (!orgResources) return;
39940
+ orgResources.workflows = (orgResources.workflows ?? []).filter(
39941
+ (w) => !remoteIds.has(w.config.resourceId)
39942
+ );
39943
+ orgResources.agents = (orgResources.agents ?? []).filter(
39944
+ (a) => !remoteIds.has(a.config.resourceId)
39945
+ );
39946
+ const remaining = (orgResources.workflows?.length ?? 0) + (orgResources.agents?.length ?? 0) + (orgResources.triggers?.length ?? 0) + (orgResources.integrations?.length ?? 0) + (orgResources.externalResources?.length ?? 0) + (orgResources.humanCheckpoints?.length ?? 0);
39947
+ if (remaining > 0) {
39948
+ this.serializedCache.set(orgName, serializeOrganization(orgResources));
39949
+ } else {
39950
+ delete this.registry[orgName];
39951
+ this.serializedCache.delete(orgName);
39952
+ }
39953
+ }
39954
+ /**
39955
+ * Get remote configuration for a specific resource
39956
+ *
39957
+ * Returns the RemoteOrgConfig if the resource was registered at runtime,
39958
+ * or null if it's a static resource or doesn't exist.
39959
+ * Used by the execution coordinator to branch between local and worker execution.
39960
+ *
39961
+ * @param orgName - Organization name
39962
+ * @param resourceId - Resource ID
39963
+ * @returns Remote config or null
39964
+ */
39965
+ getRemoteConfig(orgName, resourceId) {
39966
+ return this.remoteResources.get(`${orgName}/${resourceId}`) ?? null;
39967
+ }
39968
+ /**
39969
+ * Check if an organization has any remote (externally deployed) resources
39970
+ *
39971
+ * @param orgName - Organization name
39972
+ * @returns true if the org has at least one runtime-registered resource
39973
+ */
39974
+ isRemote(orgName) {
39975
+ const prefix = `${orgName}/`;
39976
+ for (const key of this.remoteResources.keys()) {
39977
+ if (key.startsWith(prefix)) return true;
39978
+ }
39979
+ return false;
39980
+ }
39981
+ // ============================================================================
39851
39982
  // Resource Manifest Accessors
39852
39983
  // ============================================================================
39853
39984
  /**
@@ -40044,20 +40175,11 @@ function wrapAction(commandName, fn) {
40044
40175
  // src/cli/commands/check.ts
40045
40176
  var import_meta = {};
40046
40177
  function registerCheckCommand(program3) {
40047
- program3.command("check").description("Validate project resources against the ResourceRegistry\n Example: elevasis check --config ./elevasis.config.ts --entry ./src/index.ts").option("--config <path>", "Path to elevasis.config.ts (default: ./elevasis.config.ts)").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("check", async (options) => {
40048
- const configPath = options.config ?? "./elevasis.config.ts";
40178
+ program3.command("check").description("Validate project resources against the ResourceRegistry\n Example: elevasis check --entry ./src/index.ts").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("check", async (options) => {
40049
40179
  const entryPath = options.entry ?? "./src/index.ts";
40050
40180
  const spinner = ora("Validating resources...").start();
40051
40181
  try {
40052
40182
  const jiti = (0, import_jiti.createJiti)(import_meta.url);
40053
- const configModule = await jiti.import((0, import_path.resolve)(configPath));
40054
- const config3 = configModule.default;
40055
- if (!config3?.organization) {
40056
- spinner.fail('Invalid config: missing "organization" field');
40057
- console.error(source_default.gray(` Config file: ${(0, import_path.resolve)(configPath)}`));
40058
- console.error(source_default.gray(' Expected: export default { organization: "your-org-name" }'));
40059
- throw new Error("Invalid config");
40060
- }
40061
40183
  const entryModule = await jiti.import((0, import_path.resolve)(entryPath));
40062
40184
  const org = entryModule.default;
40063
40185
  if (!org) {
@@ -40066,7 +40188,7 @@ function registerCheckCommand(program3) {
40066
40188
  console.error(source_default.gray(" Expected: export default { workflows: [...], agents: [...] }"));
40067
40189
  throw new Error("Invalid entry");
40068
40190
  }
40069
- new ResourceRegistry({ [config3.organization]: org });
40191
+ new ResourceRegistry({ _check: org });
40070
40192
  const workflowCount = org.workflows?.length ?? 0;
40071
40193
  const agentCount = org.agents?.length ?? 0;
40072
40194
  const totalCount = workflowCount + agentCount;
@@ -40117,27 +40239,73 @@ function resolveEnvironment() {
40117
40239
  return process.env.NODE_ENV === "development" ? "development" : "production";
40118
40240
  }
40119
40241
 
40242
+ // src/cli/api-client.ts
40243
+ function getApiKey() {
40244
+ const key = process.env.ELEVASIS_API_KEY;
40245
+ if (!key) {
40246
+ throw new Error(
40247
+ "ELEVASIS_API_KEY environment variable is required.\nSet it in your .env file: ELEVASIS_API_KEY=sk_..."
40248
+ );
40249
+ }
40250
+ return key;
40251
+ }
40252
+ async function apiGet(endpoint, apiUrl = resolveApiUrl()) {
40253
+ const response = await fetch(`${apiUrl}${endpoint}`, {
40254
+ headers: { Authorization: `Bearer ${getApiKey()}` }
40255
+ });
40256
+ if (!response.ok) {
40257
+ const errorText = await response.text();
40258
+ throw new Error(`API request failed (${response.status}): ${errorText}`);
40259
+ }
40260
+ if (response.status === 204) {
40261
+ return {};
40262
+ }
40263
+ return response.json();
40264
+ }
40265
+ async function apiPost(endpoint, body, apiUrl = resolveApiUrl()) {
40266
+ const response = await fetch(`${apiUrl}${endpoint}`, {
40267
+ method: "POST",
40268
+ headers: {
40269
+ Authorization: `Bearer ${getApiKey()}`,
40270
+ "Content-Type": "application/json"
40271
+ },
40272
+ body: JSON.stringify(body)
40273
+ });
40274
+ if (!response.ok) {
40275
+ const errorText = await response.text();
40276
+ throw new Error(`API request failed (${response.status}): ${errorText}`);
40277
+ }
40278
+ if (response.status === 204) {
40279
+ return {};
40280
+ }
40281
+ return response.json();
40282
+ }
40283
+
40120
40284
  // src/cli/commands/deploy.ts
40121
40285
  var import_meta2 = {};
40122
40286
  function registerDeployCommand(program3) {
40123
- program3.command("deploy").description("Validate, bundle, upload, and deploy project resources\n Example: elevasis deploy --api-url http://localhost:5170").option("--api-url <url>", "API URL").option("--config <path>", "Path to elevasis.config.ts (default: ./elevasis.config.ts)").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("deploy", async (options) => {
40287
+ program3.command("deploy").description("Validate, bundle, upload, and deploy project resources\n Example: elevasis deploy --api-url http://localhost:5170").option("--api-url <url>", "API URL").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("deploy", async (options) => {
40124
40288
  const startTime = Date.now();
40125
40289
  const apiUrl = resolveApiUrl(options.apiUrl);
40126
40290
  const env2 = resolveEnvironment();
40127
- const configPath = options.config ?? "./elevasis.config.ts";
40128
40291
  const entryPath = options.entry ?? "./src/index.ts";
40292
+ const authSpinner = ora("Authenticating...").start();
40293
+ let orgName;
40294
+ try {
40295
+ const me = await apiGet("/api/external/me", apiUrl);
40296
+ orgName = me.organizationName;
40297
+ authSpinner.succeed(
40298
+ source_default.green("Authenticating...") + source_default.white(" done") + source_default.gray(` (${orgName})`)
40299
+ );
40300
+ } catch (error46) {
40301
+ authSpinner.fail(source_default.red("Authentication failed"));
40302
+ console.error(source_default.gray(" Check your ELEVASIS_API_KEY and API URL."));
40303
+ throw error46;
40304
+ }
40129
40305
  const validateSpinner = ora("Validating...").start();
40130
- let config3;
40131
40306
  let org;
40132
40307
  try {
40133
40308
  const jiti = (0, import_jiti2.createJiti)(import_meta2.url);
40134
- const configModule = await jiti.import((0, import_path2.resolve)(configPath));
40135
- config3 = configModule.default;
40136
- if (!config3?.organization) {
40137
- validateSpinner.fail('Invalid config: missing "organization" field');
40138
- console.error(source_default.gray(` Config file: ${(0, import_path2.resolve)(configPath)}`));
40139
- throw new Error('Invalid config: missing "organization" field');
40140
- }
40141
40309
  const entryModule = await jiti.import((0, import_path2.resolve)(entryPath));
40142
40310
  org = entryModule.default;
40143
40311
  if (!org) {
@@ -40145,7 +40313,7 @@ function registerDeployCommand(program3) {
40145
40313
  console.error(source_default.gray(` Entry file: ${(0, import_path2.resolve)(entryPath)}`));
40146
40314
  throw new Error("Invalid entry: no default export found");
40147
40315
  }
40148
- new ResourceRegistry({ [config3.organization]: org });
40316
+ new ResourceRegistry({ [orgName]: org });
40149
40317
  const workflowCount = org.workflows?.length ?? 0;
40150
40318
  const agentCount = org.agents?.length ?? 0;
40151
40319
  const totalCount = workflowCount + agentCount;
@@ -40153,7 +40321,7 @@ function registerDeployCommand(program3) {
40153
40321
  source_default.green("Validating...") + source_default.white(" done") + source_default.gray(` (${totalCount} resource${totalCount !== 1 ? "s" : ""}, 0 errors)`)
40154
40322
  );
40155
40323
  console.log("");
40156
- console.log(source_default.gray(` Org: ${config3.organization}`));
40324
+ console.log(source_default.gray(` Org: ${orgName}`));
40157
40325
  console.log(source_default.gray(` Target: ${apiUrl} (${env2})`));
40158
40326
  console.log("");
40159
40327
  for (const w of org.workflows ?? []) {
@@ -40233,13 +40401,13 @@ function registerDeployCommand(program3) {
40233
40401
  console.log("");
40234
40402
  }
40235
40403
  const bundleSpinner = ora("Bundling...").start();
40236
- const wrapperPath = (0, import_path2.resolve)("__elevasis_serve.ts");
40404
+ const wrapperPath = (0, import_path2.resolve)("__elevasis_worker.ts");
40237
40405
  const bundleOutfile = (0, import_path2.resolve)("dist/bundle.js");
40238
40406
  try {
40239
40407
  const entryImport = entryPath.replace(/\.ts$/, ".js");
40240
40408
  const wrapperContent = `import org from ${JSON.stringify(entryImport)}
40241
- import { startServer } from '@elevasis/sdk/server'
40242
- startServer(org)
40409
+ import { startWorker } from '@elevasis/sdk/worker'
40410
+ startWorker(org)
40243
40411
  `;
40244
40412
  await (0, import_promises.writeFile)(wrapperPath, wrapperContent, "utf-8");
40245
40413
  await (0, import_promises.mkdir)((0, import_path2.resolve)("dist"), { recursive: true });
@@ -40329,48 +40497,6 @@ startServer(org)
40329
40497
  }));
40330
40498
  }
40331
40499
 
40332
- // src/cli/api-client.ts
40333
- function getApiKey() {
40334
- const key = process.env.ELEVASIS_API_KEY;
40335
- if (!key) {
40336
- throw new Error(
40337
- "ELEVASIS_API_KEY environment variable is required.\nSet it in your .env file: ELEVASIS_API_KEY=sk_..."
40338
- );
40339
- }
40340
- return key;
40341
- }
40342
- async function apiGet(endpoint, apiUrl = resolveApiUrl()) {
40343
- const response = await fetch(`${apiUrl}${endpoint}`, {
40344
- headers: { Authorization: `Bearer ${getApiKey()}` }
40345
- });
40346
- if (!response.ok) {
40347
- const errorText = await response.text();
40348
- throw new Error(`API request failed (${response.status}): ${errorText}`);
40349
- }
40350
- if (response.status === 204) {
40351
- return {};
40352
- }
40353
- return response.json();
40354
- }
40355
- async function apiPost(endpoint, body, apiUrl = resolveApiUrl()) {
40356
- const response = await fetch(`${apiUrl}${endpoint}`, {
40357
- method: "POST",
40358
- headers: {
40359
- Authorization: `Bearer ${getApiKey()}`,
40360
- "Content-Type": "application/json"
40361
- },
40362
- body: JSON.stringify(body)
40363
- });
40364
- if (!response.ok) {
40365
- const errorText = await response.text();
40366
- throw new Error(`API request failed (${response.status}): ${errorText}`);
40367
- }
40368
- if (response.status === 204) {
40369
- return {};
40370
- }
40371
- return response.json();
40372
- }
40373
-
40374
40500
  // src/cli/commands/exec.ts
40375
40501
  function registerExecCommand(program3) {
40376
40502
  program3.command("exec <resourceId>").description(`Execute a deployed resource
@@ -40703,7 +40829,7 @@ var import_path3 = require("path");
40703
40829
  var import_promises2 = require("fs/promises");
40704
40830
 
40705
40831
  // src/cli/version.ts
40706
- var SDK_VERSION = "0.3.3";
40832
+ var SDK_VERSION = "0.4.1";
40707
40833
 
40708
40834
  // src/cli/commands/init.ts
40709
40835
  var SCAFFOLD_FILES = [
@@ -40740,7 +40866,7 @@ function registerInitCommand(program3) {
40740
40866
  }
40741
40867
  await (0, import_promises2.mkdir)((0, import_path3.resolve)(targetDir, "src"), { recursive: true });
40742
40868
  const files = {
40743
- "elevasis.config.ts": configTemplate(orgSlug),
40869
+ "elevasis.config.ts": configTemplate(),
40744
40870
  "package.json": packageJsonTemplate(orgSlug),
40745
40871
  "pnpm-workspace.yaml": pnpmWorkspaceTemplate(),
40746
40872
  "tsconfig.json": tsconfigTemplate(),
@@ -40768,12 +40894,10 @@ function toSlug(name) {
40768
40894
  const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^[^a-z]+/, "").replace(/-+/g, "-").replace(/-$/, "");
40769
40895
  return slug.length >= 3 ? slug : "my-project";
40770
40896
  }
40771
- function configTemplate(organization) {
40897
+ function configTemplate() {
40772
40898
  return `import type { ElevasConfig } from '@elevasis/sdk'
40773
40899
 
40774
- export default {
40775
- organization: '${organization}',
40776
- } satisfies ElevasConfig
40900
+ export default {} satisfies ElevasConfig
40777
40901
  `;
40778
40902
  }
40779
40903
  function packageJsonTemplate(organization) {
@@ -40866,7 +40990,7 @@ elevasis executions <resourceId>
40866
40990
 
40867
40991
  ## Project Structure
40868
40992
 
40869
- - \`elevasis.config.ts\` -- Organization config
40993
+ - \`elevasis.config.ts\` -- Project config (optional settings)
40870
40994
  - \`src/index.ts\` -- Resource definitions (workflows, agents)
40871
40995
  - \`.env\` -- API key and environment variables
40872
40996
  `;
package/dist/index.d.ts CHANGED
@@ -1054,6 +1054,22 @@ interface IterationContext {
1054
1054
  * - Command View data generation
1055
1055
  */
1056
1056
 
1057
+ /**
1058
+ * Configuration for a remotely-deployed organization
1059
+ *
1060
+ * Stored alongside runtime-registered organizations to support
1061
+ * worker thread execution branching and credential management.
1062
+ */
1063
+ interface RemoteOrgConfig {
1064
+ /** Path to the esbuild bundle on disk */
1065
+ bundlePath: string;
1066
+ /** Deployment record ID */
1067
+ deploymentId: string;
1068
+ /** Developer-provided environment variables injected into the worker */
1069
+ envVars?: Record<string, string>;
1070
+ /** Platform tool name -> credential name mapping */
1071
+ toolCredentials?: Record<string, string>;
1072
+ }
1057
1073
  /**
1058
1074
  * Organization-specific resource collection
1059
1075
  *
@@ -1084,9 +1100,16 @@ declare class ResourceRegistry {
1084
1100
  private registry;
1085
1101
  /**
1086
1102
  * Pre-serialized organization data cache
1087
- * Computed once at construction, never invalidated (static registry)
1103
+ * Computed once at construction for static orgs, updated incrementally for runtime orgs
1088
1104
  */
1089
1105
  private serializedCache;
1106
+ /**
1107
+ * Per-resource remote configuration (external deployments)
1108
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
1109
+ * Tracks which individual resources were added at runtime via deploy pipeline.
1110
+ * Static and remote resources coexist in the same org.
1111
+ */
1112
+ private remoteResources;
1090
1113
  constructor(registry: OrganizationRegistry);
1091
1114
  /**
1092
1115
  * Validates registry on construction
@@ -1122,6 +1145,56 @@ declare class ResourceRegistry {
1122
1145
  * NOTE: For debugging only - returns raw registry data
1123
1146
  */
1124
1147
  listAllResources(): OrganizationRegistry;
1148
+ /**
1149
+ * Register external resources at runtime
1150
+ *
1151
+ * Called during deploy pipeline when an external developer deploys their bundle.
1152
+ * Merges the incoming stub definitions into the org's registry and stores
1153
+ * per-resource remote config for worker thread execution branching.
1154
+ *
1155
+ * Static and remote resources coexist in the same org. If the org already
1156
+ * has static resources, the incoming remote resources are merged alongside them.
1157
+ * If redeploying (some resources already registered as remote for this org),
1158
+ * the previous remote resources are unregistered first.
1159
+ *
1160
+ * @param orgName - Organization name (used as registry key)
1161
+ * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
1162
+ * @param remote - Remote configuration (bundle path, deployment ID, env vars)
1163
+ * @throws Error if incoming resourceId conflicts with a static resource
1164
+ * @throws Error if incoming deployment contains duplicate resourceIds
1165
+ */
1166
+ registerOrganization(orgName: string, org: OrganizationResources, remote: RemoteOrgConfig): void;
1167
+ /**
1168
+ * Unregister runtime-registered resources for an organization
1169
+ *
1170
+ * Removes only resources that were registered at runtime (via registerOrganization).
1171
+ * Static resources loaded at startup are preserved. If the org still has static
1172
+ * resources after removal, the serialization cache is rebuilt. If no resources
1173
+ * remain, the org is fully removed from the registry.
1174
+ * No-op if the org has no remote resources.
1175
+ *
1176
+ * @param orgName - Organization name to unregister remote resources from
1177
+ */
1178
+ unregisterOrganization(orgName: string): void;
1179
+ /**
1180
+ * Get remote configuration for a specific resource
1181
+ *
1182
+ * Returns the RemoteOrgConfig if the resource was registered at runtime,
1183
+ * or null if it's a static resource or doesn't exist.
1184
+ * Used by the execution coordinator to branch between local and worker execution.
1185
+ *
1186
+ * @param orgName - Organization name
1187
+ * @param resourceId - Resource ID
1188
+ * @returns Remote config or null
1189
+ */
1190
+ getRemoteConfig(orgName: string, resourceId: string): RemoteOrgConfig | null;
1191
+ /**
1192
+ * Check if an organization has any remote (externally deployed) resources
1193
+ *
1194
+ * @param orgName - Organization name
1195
+ * @returns true if the org has at least one runtime-registered resource
1196
+ */
1197
+ isRemote(orgName: string): boolean;
1125
1198
  /**
1126
1199
  * Get triggers for an organization
1127
1200
  * @param organizationName - Organization name
@@ -1837,10 +1910,10 @@ declare class RegistryValidationError extends Error {
1837
1910
 
1838
1911
  /**
1839
1912
  * Project configuration for an external developer project.
1840
- * Defined in elevas.config.ts at the project root.
1913
+ * Defined in elevasis.config.ts at the project root.
1914
+ * Organization is derived from the ELEVASIS_API_KEY -- not configured here.
1841
1915
  */
1842
1916
  interface ElevasConfig {
1843
- organization: string;
1844
1917
  defaultStatus?: ResourceStatus;
1845
1918
  dev?: {
1846
1919
  port?: number;
package/dist/index.js CHANGED
@@ -3162,9 +3162,16 @@ var ResourceRegistry = class {
3162
3162
  }
3163
3163
  /**
3164
3164
  * Pre-serialized organization data cache
3165
- * Computed once at construction, never invalidated (static registry)
3165
+ * Computed once at construction for static orgs, updated incrementally for runtime orgs
3166
3166
  */
3167
3167
  serializedCache;
3168
+ /**
3169
+ * Per-resource remote configuration (external deployments)
3170
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
3171
+ * Tracks which individual resources were added at runtime via deploy pipeline.
3172
+ * Static and remote resources coexist in the same org.
3173
+ */
3174
+ remoteResources = /* @__PURE__ */ new Map();
3168
3175
  /**
3169
3176
  * Validates registry on construction
3170
3177
  * - Checks for duplicate resourceIds within organizations
@@ -3264,6 +3271,130 @@ var ResourceRegistry = class {
3264
3271
  return this.registry;
3265
3272
  }
3266
3273
  // ============================================================================
3274
+ // Runtime Organization Registration (External Deployments)
3275
+ // ============================================================================
3276
+ /**
3277
+ * Register external resources at runtime
3278
+ *
3279
+ * Called during deploy pipeline when an external developer deploys their bundle.
3280
+ * Merges the incoming stub definitions into the org's registry and stores
3281
+ * per-resource remote config for worker thread execution branching.
3282
+ *
3283
+ * Static and remote resources coexist in the same org. If the org already
3284
+ * has static resources, the incoming remote resources are merged alongside them.
3285
+ * If redeploying (some resources already registered as remote for this org),
3286
+ * the previous remote resources are unregistered first.
3287
+ *
3288
+ * @param orgName - Organization name (used as registry key)
3289
+ * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
3290
+ * @param remote - Remote configuration (bundle path, deployment ID, env vars)
3291
+ * @throws Error if incoming resourceId conflicts with a static resource
3292
+ * @throws Error if incoming deployment contains duplicate resourceIds
3293
+ */
3294
+ registerOrganization(orgName, org, remote) {
3295
+ const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId);
3296
+ const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId);
3297
+ const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds];
3298
+ const seen = /* @__PURE__ */ new Set();
3299
+ for (const id of incomingIds) {
3300
+ if (seen.has(id)) {
3301
+ throw new Error(
3302
+ `Duplicate resource ID '${id}' in deployment. Each resource must have a unique ID.`
3303
+ );
3304
+ }
3305
+ seen.add(id);
3306
+ }
3307
+ if (this.isRemote(orgName)) {
3308
+ this.unregisterOrganization(orgName);
3309
+ }
3310
+ const existingOrg = this.registry[orgName];
3311
+ if (existingOrg) {
3312
+ const staticWorkflowIds = new Set((existingOrg.workflows ?? []).map((w) => w.config.resourceId));
3313
+ const staticAgentIds = new Set((existingOrg.agents ?? []).map((a) => a.config.resourceId));
3314
+ for (const id of incomingIds) {
3315
+ if (staticWorkflowIds.has(id) || staticAgentIds.has(id)) {
3316
+ throw new Error(
3317
+ `Resource '${id}' already exists in '${orgName}' as an internal resource. External deployments cannot override internal resources.`
3318
+ );
3319
+ }
3320
+ }
3321
+ }
3322
+ if (existingOrg) {
3323
+ existingOrg.workflows = [...existingOrg.workflows ?? [], ...org.workflows ?? []];
3324
+ existingOrg.agents = [...existingOrg.agents ?? [], ...org.agents ?? []];
3325
+ } else {
3326
+ this.registry[orgName] = org;
3327
+ }
3328
+ for (const id of incomingIds) {
3329
+ this.remoteResources.set(`${orgName}/${id}`, remote);
3330
+ }
3331
+ this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]));
3332
+ }
3333
+ /**
3334
+ * Unregister runtime-registered resources for an organization
3335
+ *
3336
+ * Removes only resources that were registered at runtime (via registerOrganization).
3337
+ * Static resources loaded at startup are preserved. If the org still has static
3338
+ * resources after removal, the serialization cache is rebuilt. If no resources
3339
+ * remain, the org is fully removed from the registry.
3340
+ * No-op if the org has no remote resources.
3341
+ *
3342
+ * @param orgName - Organization name to unregister remote resources from
3343
+ */
3344
+ unregisterOrganization(orgName) {
3345
+ const prefix = `${orgName}/`;
3346
+ const remoteIds = /* @__PURE__ */ new Set();
3347
+ for (const key of this.remoteResources.keys()) {
3348
+ if (key.startsWith(prefix)) {
3349
+ remoteIds.add(key.slice(prefix.length));
3350
+ this.remoteResources.delete(key);
3351
+ }
3352
+ }
3353
+ if (remoteIds.size === 0) return;
3354
+ const orgResources = this.registry[orgName];
3355
+ if (!orgResources) return;
3356
+ orgResources.workflows = (orgResources.workflows ?? []).filter(
3357
+ (w) => !remoteIds.has(w.config.resourceId)
3358
+ );
3359
+ orgResources.agents = (orgResources.agents ?? []).filter(
3360
+ (a) => !remoteIds.has(a.config.resourceId)
3361
+ );
3362
+ const remaining = (orgResources.workflows?.length ?? 0) + (orgResources.agents?.length ?? 0) + (orgResources.triggers?.length ?? 0) + (orgResources.integrations?.length ?? 0) + (orgResources.externalResources?.length ?? 0) + (orgResources.humanCheckpoints?.length ?? 0);
3363
+ if (remaining > 0) {
3364
+ this.serializedCache.set(orgName, serializeOrganization(orgResources));
3365
+ } else {
3366
+ delete this.registry[orgName];
3367
+ this.serializedCache.delete(orgName);
3368
+ }
3369
+ }
3370
+ /**
3371
+ * Get remote configuration for a specific resource
3372
+ *
3373
+ * Returns the RemoteOrgConfig if the resource was registered at runtime,
3374
+ * or null if it's a static resource or doesn't exist.
3375
+ * Used by the execution coordinator to branch between local and worker execution.
3376
+ *
3377
+ * @param orgName - Organization name
3378
+ * @param resourceId - Resource ID
3379
+ * @returns Remote config or null
3380
+ */
3381
+ getRemoteConfig(orgName, resourceId) {
3382
+ return this.remoteResources.get(`${orgName}/${resourceId}`) ?? null;
3383
+ }
3384
+ /**
3385
+ * Check if an organization has any remote (externally deployed) resources
3386
+ *
3387
+ * @param orgName - Organization name
3388
+ * @returns true if the org has at least one runtime-registered resource
3389
+ */
3390
+ isRemote(orgName) {
3391
+ const prefix = `${orgName}/`;
3392
+ for (const key of this.remoteResources.keys()) {
3393
+ if (key.startsWith(prefix)) return true;
3394
+ }
3395
+ return false;
3396
+ }
3397
+ // ============================================================================
3267
3398
  // Resource Manifest Accessors
3268
3399
  // ============================================================================
3269
3400
  /**
@@ -0,0 +1,132 @@
1
+ import { parentPort } from 'worker_threads';
2
+
3
+ // src/worker/index.ts
4
+ function resolveNext(next, data) {
5
+ if (next === null) return null;
6
+ if (next.type === "linear") return next.target;
7
+ for (const route of next.routes) {
8
+ if (route.condition(data)) return route.target;
9
+ }
10
+ return next.default;
11
+ }
12
+ async function executeWorkflow(workflow, input) {
13
+ const logs = [];
14
+ const origLog = console.log;
15
+ const origWarn = console.warn;
16
+ const origError = console.error;
17
+ const capture = (level, orig) => (...args) => {
18
+ logs.push({ level, message: args.map(String).join(" ") });
19
+ orig(...args);
20
+ };
21
+ console.log = capture("info", origLog);
22
+ console.warn = capture("warn", origWarn);
23
+ console.error = capture("error", origError);
24
+ try {
25
+ let currentData = workflow.contract.inputSchema ? workflow.contract.inputSchema.parse(input) : input;
26
+ let stepId = workflow.entryPoint;
27
+ while (stepId !== null) {
28
+ const step = workflow.steps[stepId];
29
+ if (!step) {
30
+ throw new Error(`Step '${stepId}' not found in workflow '${workflow.config.resourceId}'`);
31
+ }
32
+ const stepInput = step.inputSchema.parse(currentData);
33
+ const rawOutput = await step.handler(stepInput, {
34
+ executionId: "",
35
+ organizationId: "",
36
+ organizationName: "",
37
+ resourceId: workflow.config.resourceId,
38
+ executionDepth: 0,
39
+ store: /* @__PURE__ */ new Map(),
40
+ logger: {
41
+ debug: (msg) => console.log(`[debug] ${msg}`),
42
+ info: (msg) => console.log(`[info] ${msg}`),
43
+ warn: (msg) => console.warn(`[warn] ${msg}`),
44
+ error: (msg) => console.error(`[error] ${msg}`)
45
+ }
46
+ });
47
+ currentData = step.outputSchema.parse(rawOutput);
48
+ stepId = resolveNext(step.next, currentData);
49
+ }
50
+ if (workflow.contract.outputSchema) {
51
+ currentData = workflow.contract.outputSchema.parse(currentData);
52
+ }
53
+ return { output: currentData, logs };
54
+ } finally {
55
+ console.log = origLog;
56
+ console.warn = origWarn;
57
+ console.error = origError;
58
+ }
59
+ }
60
+ function startWorker(org) {
61
+ const workflows = new Map(
62
+ (org.workflows ?? []).map((w) => [w.config.resourceId, w])
63
+ );
64
+ const agents = new Map(
65
+ (org.agents ?? []).map((a) => [a.config.resourceId, a])
66
+ );
67
+ console.log(`[SDK-WORKER] Worker started with ${workflows.size} workflow(s), ${agents.size} agent(s)`);
68
+ parentPort.on("message", async (msg) => {
69
+ if (msg.type === "manifest") {
70
+ const workflowList = (org.workflows ?? []).map((w) => w.config.resourceId);
71
+ const agentList = (org.agents ?? []).map((a) => a.config.resourceId);
72
+ console.log(`[SDK-WORKER] Manifest requested: workflows=[${workflowList.join(", ")}], agents=[${agentList.join(", ")}]`);
73
+ parentPort.postMessage({
74
+ type: "manifest",
75
+ workflows: (org.workflows ?? []).map((w) => ({
76
+ resourceId: w.config.resourceId,
77
+ name: w.config.name,
78
+ type: w.config.type,
79
+ status: w.config.status,
80
+ description: w.config.description,
81
+ version: w.config.version
82
+ })),
83
+ agents: (org.agents ?? []).map((a) => ({
84
+ resourceId: a.config.resourceId,
85
+ name: a.config.name,
86
+ type: a.config.type,
87
+ status: a.config.status,
88
+ description: a.config.description,
89
+ version: a.config.version
90
+ }))
91
+ });
92
+ return;
93
+ }
94
+ if (msg.type === "execute") {
95
+ const { resourceId, executionId, input } = msg;
96
+ console.log(`[SDK-WORKER] Execute request: resourceId=${resourceId}, executionId=${executionId}`);
97
+ const workflow = workflows.get(resourceId);
98
+ if (workflow) {
99
+ try {
100
+ console.log(`[SDK-WORKER] Running workflow '${resourceId}' (${Object.keys(workflow.steps).length} steps)`);
101
+ const startTime = Date.now();
102
+ const { output, logs } = await executeWorkflow(workflow, input);
103
+ console.log(`[SDK-WORKER] Workflow '${resourceId}' completed (${Date.now() - startTime}ms)`);
104
+ parentPort.postMessage({ type: "result", status: "completed", output, logs });
105
+ } catch (err) {
106
+ console.error(`[SDK-WORKER] Workflow '${resourceId}' failed: ${String(err)}`);
107
+ parentPort.postMessage({ type: "result", status: "failed", error: String(err), logs: [] });
108
+ }
109
+ return;
110
+ }
111
+ if (agents.has(resourceId)) {
112
+ console.error(`[SDK-WORKER] Agent execution not supported: ${resourceId}`);
113
+ parentPort.postMessage({
114
+ type: "result",
115
+ status: "failed",
116
+ error: "Agent execution not yet supported in worker runtime",
117
+ logs: []
118
+ });
119
+ return;
120
+ }
121
+ console.error(`[SDK-WORKER] Resource not found: ${resourceId}`);
122
+ parentPort.postMessage({
123
+ type: "result",
124
+ status: "failed",
125
+ error: `Resource not found: ${resourceId}`,
126
+ logs: []
127
+ });
128
+ }
129
+ });
130
+ }
131
+
132
+ export { startWorker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/sdk",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "SDK for building Elevasis organization resources",
5
5
  "comment:bin": "IMPORTANT: This package shares the 'elevasis' binary name with @repo/cli. They never conflict because @elevasis/sdk must NEVER be added as a dependency of any workspace package (apps/*, packages/*, organizations/*). Workspace projects use @repo/cli for the 'elevasis' binary. External developers (outside the workspace) get this SDK's binary via npm install.",
6
6
  "type": "module",
@@ -13,14 +13,14 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
15
  },
16
- "./server": {
17
- "import": "./dist/server/index.js"
16
+ "./worker": {
17
+ "import": "./dist/worker/index.js"
18
18
  }
19
19
  },
20
20
  "files": [
21
21
  "dist/index.js",
22
22
  "dist/index.d.ts",
23
- "dist/server/index.js",
23
+ "dist/worker/index.js",
24
24
  "dist/cli.cjs"
25
25
  ],
26
26
  "scripts": {
@@ -1,174 +0,0 @@
1
- import { createServer } from 'http';
2
-
3
- // src/server/index.ts
4
- function readBody(req) {
5
- return new Promise((resolve, reject) => {
6
- const chunks = [];
7
- req.on("data", (chunk) => chunks.push(chunk));
8
- req.on("end", () => resolve(Buffer.concat(chunks).toString()));
9
- req.on("error", reject);
10
- });
11
- }
12
- function json(res, status, data) {
13
- res.writeHead(status, { "Content-Type": "application/json" });
14
- res.end(JSON.stringify(data));
15
- }
16
- function resolveNext(next, data) {
17
- if (next === null) return null;
18
- if (next.type === "linear") return next.target;
19
- for (const route of next.routes) {
20
- if (route.condition(data)) return route.target;
21
- }
22
- return next.default;
23
- }
24
- async function executeWorkflow(workflow, input, context) {
25
- let currentData = workflow.contract.inputSchema.parse(input);
26
- let stepId = workflow.entryPoint;
27
- while (stepId !== null) {
28
- if (context.signal?.aborted) {
29
- throw new Error("Execution cancelled");
30
- }
31
- const step = workflow.steps[stepId];
32
- if (!step) {
33
- throw new Error(`Step '${stepId}' not found in workflow '${workflow.config.resourceId}'`);
34
- }
35
- const stepInput = step.inputSchema.parse(currentData);
36
- const rawOutput = await step.handler(stepInput, context);
37
- currentData = step.outputSchema.parse(rawOutput);
38
- stepId = resolveNext(step.next, currentData);
39
- }
40
- if (workflow.contract.outputSchema) {
41
- currentData = workflow.contract.outputSchema.parse(currentData);
42
- }
43
- return currentData;
44
- }
45
- function withLogCapture(fn) {
46
- const logs = [];
47
- const orig = { log: console.log, warn: console.warn, error: console.error };
48
- const capture = (level, origFn) => (...args) => {
49
- logs.push({ level, message: args.map(String).join(" "), timestamp: Date.now() });
50
- origFn(...args);
51
- };
52
- console.log = capture("info", orig.log);
53
- console.warn = capture("warn", orig.warn);
54
- console.error = capture("error", orig.error);
55
- return fn().then((result) => ({ result, logs })).finally(() => {
56
- console.log = orig.log;
57
- console.warn = orig.warn;
58
- console.error = orig.error;
59
- });
60
- }
61
- function startServer(org) {
62
- const port = parseInt(process.env.PORT || "3000");
63
- const workflows = new Map(
64
- (org.workflows ?? []).map((w) => [w.config.resourceId, w])
65
- );
66
- const agents = new Map(
67
- (org.agents ?? []).map((a) => [a.config.resourceId, a])
68
- );
69
- const controllers = /* @__PURE__ */ new Map();
70
- const server = createServer(async (req, res) => {
71
- try {
72
- if (req.url === "/health" && req.method === "GET") {
73
- res.writeHead(200).end("ok");
74
- return;
75
- }
76
- if (req.url === "/manifest" && req.method === "GET") {
77
- json(res, 200, {
78
- workflows: (org.workflows ?? []).map((w) => ({
79
- resourceId: w.config.resourceId,
80
- name: w.config.name,
81
- type: w.config.type,
82
- status: w.config.status,
83
- description: w.config.description,
84
- version: w.config.version
85
- })),
86
- agents: (org.agents ?? []).map((a) => ({
87
- resourceId: a.config.resourceId,
88
- name: a.config.name,
89
- type: a.config.type,
90
- status: a.config.status,
91
- description: a.config.description,
92
- version: a.config.version
93
- }))
94
- });
95
- return;
96
- }
97
- if (req.url === "/execute" && req.method === "POST") {
98
- const body = JSON.parse(await readBody(req));
99
- const { resourceId, executionId, input } = body;
100
- const workflow = workflows.get(resourceId);
101
- if (workflow) {
102
- const controller = new AbortController();
103
- controllers.set(executionId, controller);
104
- const deadline = req.headers["x-elevasis-deadline"];
105
- const timer = deadline ? setTimeout(() => controller.abort(), Number(deadline) - Date.now()) : void 0;
106
- const context = {
107
- executionId,
108
- organizationId: "",
109
- organizationName: "",
110
- resourceId,
111
- executionDepth: 0,
112
- store: /* @__PURE__ */ new Map(),
113
- signal: controller.signal,
114
- logger: {
115
- debug: (msg) => console.log(`[debug] ${msg}`),
116
- info: (msg) => console.log(`[info] ${msg}`),
117
- warn: (msg) => console.warn(`[warn] ${msg}`),
118
- error: (msg) => console.error(`[error] ${msg}`)
119
- }
120
- };
121
- try {
122
- const { result: output, logs } = await withLogCapture(
123
- () => executeWorkflow(workflow, input, context)
124
- );
125
- json(res, 200, { status: "completed", output, logs });
126
- } catch (err) {
127
- const { logs } = await withLogCapture(async () => {
128
- });
129
- json(res, 500, { status: "failed", error: String(err), logs });
130
- } finally {
131
- controllers.delete(executionId);
132
- if (timer) clearTimeout(timer);
133
- }
134
- return;
135
- }
136
- const agent = agents.get(resourceId);
137
- if (agent) {
138
- json(res, 501, {
139
- status: "failed",
140
- error: `Agent execution is not supported in SDK server v0.3.0. Resource '${resourceId}' is an agent.`,
141
- logs: []
142
- });
143
- return;
144
- }
145
- json(res, 404, { error: `Resource not found: ${resourceId}` });
146
- return;
147
- }
148
- if (req.url === "/cancel" && req.method === "POST") {
149
- const body = JSON.parse(await readBody(req));
150
- const { executionId } = body;
151
- const controller = controllers.get(executionId);
152
- if (controller) {
153
- controller.abort();
154
- controllers.delete(executionId);
155
- json(res, 200, { cancelled: true });
156
- } else {
157
- json(res, 404, { error: `No running execution: ${executionId}` });
158
- }
159
- return;
160
- }
161
- res.writeHead(404).end();
162
- } catch (err) {
163
- console.error("Unhandled server error:", err);
164
- if (!res.headersSent) {
165
- json(res, 500, { error: "Internal server error" });
166
- }
167
- }
168
- });
169
- server.listen(port, () => {
170
- console.log(`Elevasis SDK server listening on port ${port}`);
171
- });
172
- }
173
-
174
- export { startServer };