@primitivedotdev/cli 0.35.1 → 0.36.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.
@@ -1090,6 +1090,24 @@ function acquireCliCredentialsLock(configDir, options = {}) {
1090
1090
  });
1091
1091
  };
1092
1092
  }
1093
+ /**
1094
+ * Detect the PRIMITIVE_KEY vs PRIMITIVE_API_KEY rename trap.
1095
+ *
1096
+ * AGX feedback: users on older docs (or coming from other tools) set
1097
+ * `PRIMITIVE_KEY` and then cannot figure out why the CLI reports no
1098
+ * API key. The CLI reads `PRIMITIVE_API_KEY` only. Returns a stderr
1099
+ * hint when `PRIMITIVE_KEY` is set but `PRIMITIVE_API_KEY` is not,
1100
+ * otherwise null. Exported as a helper so both `doctor` and the
1101
+ * general auth-resolution path surface the same guidance from the
1102
+ * first command the user runs, instead of forcing them to discover
1103
+ * the rename via `doctor` after the fact.
1104
+ */
1105
+ function detectPrimitiveKeyEnvMisname(env = process.env) {
1106
+ const primitiveKey = env.PRIMITIVE_KEY;
1107
+ const primitiveApiKey = env.PRIMITIVE_API_KEY;
1108
+ if ((primitiveKey?.length ?? 0) > 0 && (primitiveApiKey?.length ?? 0) === 0) return "PRIMITIVE_KEY is set but the CLI reads PRIMITIVE_API_KEY. Rename your env var, or re-run with PRIMITIVE_API_KEY=$PRIMITIVE_KEY.";
1109
+ return null;
1110
+ }
1093
1111
  function resolveCliAuth(params) {
1094
1112
  const apiKey = params.apiKey?.trim();
1095
1113
  const apiBaseUrl = normalizeApiBaseUrl(params.apiBaseUrl);
@@ -1293,4 +1311,4 @@ function redactCliEnvironment(environment) {
1293
1311
  };
1294
1312
  }
1295
1313
  //#endregion
1296
- export { createConfig as A, chatStatePath as C, saveActiveChatState as D, loadChatConversationByLocalId as E, PrimitiveApiClient as O, saveCliCredentials as S, loadActiveChatState as T, deleteCliCredentials as _, normalizeCliEnvironmentName as a, normalizeApiBaseUrl as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, createClient as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, deleteChatState as w, resolveCliAuth as x, loadCliCredentials as y };
1314
+ export { createClient as A, saveCliCredentials as C, loadChatConversationByLocalId as D, loadActiveChatState as E, saveActiveChatState as O, resolveCliAuth as S, deleteChatState as T, deleteCliCredentials as _, normalizeCliEnvironmentName as a, loadCliCredentials as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, createConfig as j, PrimitiveApiClient as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, chatStatePath as w, normalizeApiBaseUrl as x, detectPrimitiveKeyEnvMisname as y };
@@ -1,4 +1,4 @@
1
- import { A as createConfig, C as chatStatePath, D as saveActiveChatState, E as loadChatConversationByLocalId, O as PrimitiveApiClient, S as saveCliCredentials, T as loadActiveChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as normalizeApiBaseUrl, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, k as createClient, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as deleteChatState, x as resolveCliAuth, y as loadCliCredentials } from "../cli-config-DREZ2BxT.js";
1
+ import { A as createClient, C as saveCliCredentials, D as loadChatConversationByLocalId, E as loadActiveChatState, O as saveActiveChatState, S as resolveCliAuth, T as deleteChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as loadCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createConfig, k as PrimitiveApiClient, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as chatStatePath, x as normalizeApiBaseUrl, y as detectPrimitiveKeyEnvMisname } from "../cli-config-D7wN_PBc.js";
2
2
  import { Args, Command, Errors, Flags, ux } from "@oclif/core";
3
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
@@ -44,8 +44,10 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
44
44
  getConversation: () => getConversation,
45
45
  getEmail: () => getEmail,
46
46
  getFunction: () => getFunction,
47
+ getFunctionRouting: () => getFunctionRouting,
47
48
  getFunctionTestRunTrace: () => getFunctionTestRunTrace,
48
49
  getInboxStatus: () => getInboxStatus,
50
+ getOrgRoutingTopology: () => getOrgRoutingTopology,
49
51
  getSendPermissions: () => getSendPermissions,
50
52
  getSentEmail: () => getSentEmail,
51
53
  getStorageStats: () => getStorageStats,
@@ -70,12 +72,14 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
70
72
  searchEmails: () => searchEmails,
71
73
  semanticSearch: () => semanticSearch,
72
74
  sendEmail: () => sendEmail,
75
+ setFunctionRoute: () => setFunctionRoute,
73
76
  setFunctionSecret: () => setFunctionSecret,
74
77
  startAgentSignup: () => startAgentSignup,
75
78
  startCliLogin: () => startCliLogin,
76
79
  startCliSignup: () => startCliSignup,
77
80
  testEndpoint: () => testEndpoint,
78
81
  testFunction: () => testFunction,
82
+ unsetFunctionRoute: () => unsetFunctionRoute,
79
83
  updateAccount: () => updateAccount,
80
84
  updateDomain: () => updateDomain,
81
85
  updateEndpoint: () => updateEndpoint,
@@ -1102,11 +1106,13 @@ const listFunctions = (options) => (options?.client ?? client).get({
1102
1106
  * attempt, and sent to the runtime so stack traces can resolve to
1103
1107
  * original source files.
1104
1108
  *
1105
- * **Auto-wiring.** On successful deploy, Primitive automatically
1106
- * creates a webhook endpoint that delivers inbound mail to the
1107
- * function. There is nothing to configure on the Endpoints API
1108
- * for this to work; the internal runtime URL is not returned by
1109
- * the API and is not a customer-facing integration surface.
1109
+ * **Routing.** On successful deploy, the function code is live
1110
+ * in the runtime, but inbound mail will not reach it until at
1111
+ * least one route is bound. Routes are managed from the Primitive
1112
+ * dashboard. A `deploy_status` of `deployed` means the script is
1113
+ * installed, not that the function is receiving mail. The
1114
+ * internal runtime URL is not returned by the API and is not a
1115
+ * customer-facing integration surface.
1110
1116
  *
1111
1117
  * **Secrets.** New functions ship with the managed secrets
1112
1118
  * (`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,
@@ -1131,7 +1137,7 @@ const createFunction = (options) => (options.client ?? client).post({
1131
1137
  * Delete a function
1132
1138
  *
1133
1139
  * Soft-deletes the function row, removes the script from the edge
1134
- * runtime, and deactivates the auto-wired webhook endpoint so no
1140
+ * runtime, and deactivates any route bound to this function so no
1135
1141
  * further inbound mail is delivered. Past deploy history,
1136
1142
  * invocations, and logs are retained.
1137
1143
  *
@@ -1248,6 +1254,80 @@ const getFunctionTestRunTrace = (options) => (options.client ?? client).get({
1248
1254
  ...options
1249
1255
  });
1250
1256
  /**
1257
+ * Get the org's function routing topology
1258
+ *
1259
+ * Returns a single snapshot of how inbound mail is routed across
1260
+ * this org's active domains and functions: which active domain has
1261
+ * which function bound, the org's fallback function (if any), and
1262
+ * every deployed function with no route bound. Use this to answer
1263
+ * "which of my functions actually receive mail?" diagnostically.
1264
+ *
1265
+ */
1266
+ const getOrgRoutingTopology = (options) => (options?.client ?? client).get({
1267
+ security: [{
1268
+ scheme: "bearer",
1269
+ type: "http"
1270
+ }],
1271
+ url: "/functions/routing-topology",
1272
+ ...options
1273
+ });
1274
+ /**
1275
+ * Get a function's current route binding
1276
+ *
1277
+ * Returns the endpoint binding for the function, or null when no
1278
+ * route is currently bound. The binding identifies whether the
1279
+ * function receives mail for a specific domain (scoped) or for any
1280
+ * active domain that has no scoped binding (fallback).
1281
+ *
1282
+ */
1283
+ const getFunctionRouting = (options) => (options.client ?? client).get({
1284
+ security: [{
1285
+ scheme: "bearer",
1286
+ type: "http"
1287
+ }],
1288
+ url: "/functions/{id}/routing",
1289
+ ...options
1290
+ });
1291
+ /**
1292
+ * Unbind any route from a function
1293
+ *
1294
+ * Deactivates every active endpoint bound to this function. The
1295
+ * function stays deployed but stops receiving inbound mail. Safe
1296
+ * to call when no route is currently bound (no-op).
1297
+ *
1298
+ */
1299
+ const unsetFunctionRoute = (options) => (options.client ?? client).delete({
1300
+ security: [{
1301
+ scheme: "bearer",
1302
+ type: "http"
1303
+ }],
1304
+ url: "/functions/{id}/route",
1305
+ ...options
1306
+ });
1307
+ /**
1308
+ * Bind a route to a function
1309
+ *
1310
+ * Binds inbound mail to this function. The route target is either
1311
+ * a specific verified domain (scoped) or the org's fallback (any
1312
+ * active domain with no scoped binding). If another function is
1313
+ * already bound at the target, returns a `conflict` envelope
1314
+ * describing the holder; re-issue with `takeover: true` to
1315
+ * deactivate that prior binding and install this one.
1316
+ *
1317
+ */
1318
+ const setFunctionRoute = (options) => (options.client ?? client).put({
1319
+ security: [{
1320
+ scheme: "bearer",
1321
+ type: "http"
1322
+ }],
1323
+ url: "/functions/{id}/route",
1324
+ ...options,
1325
+ headers: {
1326
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
1327
+ ...options.headers
1328
+ }
1329
+ });
1330
+ /**
1251
1331
  * List a function's secrets
1252
1332
  *
1253
1333
  * Returns metadata for every secret bound to the function, with
@@ -3029,7 +3109,7 @@ const openapiDocument = {
3029
3109
  "post": {
3030
3110
  "operationId": "createFunction",
3031
3111
  "summary": "Deploy a function",
3032
- "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). Primitive signs\neach delivery and forwards the `Primitive-Signature` header to\nthe handler. Verify the raw request body with\n`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification\nthe request body parses to an `email.received` event (see\n`EmailReceivedEvent` and the Webhook payload section for the full\nschema). Code is bundled before being uploaded; ship a single\nself-contained file rather than relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the internal runtime URL is not returned by\nthe API and is not a customer-facing integration surface.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,\n`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
3112
+ "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). Primitive signs\neach delivery and forwards the `Primitive-Signature` header to\nthe handler. Verify the raw request body with\n`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification\nthe request body parses to an `email.received` event (see\n`EmailReceivedEvent` and the Webhook payload section for the full\nschema). Code is bundled before being uploaded; ship a single\nself-contained file rather than relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Routing.** On successful deploy, the function code is live\nin the runtime, but inbound mail will not reach it until at\nleast one route is bound. Routes are managed from the Primitive\ndashboard. A `deploy_status` of `deployed` means the script is\ninstalled, not that the function is receiving mail. The\ninternal runtime URL is not returned by the API and is not a\ncustomer-facing integration surface.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,\n`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
3033
3113
  "tags": ["Functions"],
3034
3114
  "requestBody": {
3035
3115
  "required": true,
@@ -3108,7 +3188,7 @@ const openapiDocument = {
3108
3188
  "delete": {
3109
3189
  "operationId": "deleteFunction",
3110
3190
  "summary": "Delete a function",
3111
- "description": "Soft-deletes the function row, removes the script from the edge\nruntime, and deactivates the auto-wired webhook endpoint so no\nfurther inbound mail is delivered. Past deploy history,\ninvocations, and logs are retained.\n\nReturns 502 if the runtime delete fails partway; the function\nrow stays in place and the call is safe to retry until it\nsucceeds.\n",
3191
+ "description": "Soft-deletes the function row, removes the script from the edge\nruntime, and deactivates any route bound to this function so no\nfurther inbound mail is delivered. Past deploy history,\ninvocations, and logs are retained.\n\nReturns 502 if the runtime delete fails partway; the function\nrow stays in place and the call is safe to retry until it\nsucceeds.\n",
3112
3192
  "tags": ["Functions"],
3113
3193
  "responses": {
3114
3194
  "200": { "$ref": "#/components/responses/Deleted" },
@@ -3193,6 +3273,92 @@ const openapiDocument = {
3193
3273
  }
3194
3274
  }
3195
3275
  },
3276
+ "/functions/routing-topology": { "get": {
3277
+ "operationId": "getOrgRoutingTopology",
3278
+ "summary": "Get the org's function routing topology",
3279
+ "description": "Returns a single snapshot of how inbound mail is routed across\nthis org's active domains and functions: which active domain has\nwhich function bound, the org's fallback function (if any), and\nevery deployed function with no route bound. Use this to answer\n\"which of my functions actually receive mail?\" diagnostically.\n",
3280
+ "tags": ["Functions"],
3281
+ "responses": {
3282
+ "200": {
3283
+ "description": "Routing topology",
3284
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
3285
+ "type": "object",
3286
+ "properties": { "data": { "$ref": "#/components/schemas/RoutingTopology" } }
3287
+ }] } } }
3288
+ },
3289
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3290
+ "403": { "$ref": "#/components/responses/Forbidden" }
3291
+ }
3292
+ } },
3293
+ "/functions/{id}/routing": {
3294
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3295
+ "get": {
3296
+ "operationId": "getFunctionRouting",
3297
+ "summary": "Get a function's current route binding",
3298
+ "description": "Returns the endpoint binding for the function, or null when no\nroute is currently bound. The binding identifies whether the\nfunction receives mail for a specific domain (scoped) or for any\nactive domain that has no scoped binding (fallback).\n",
3299
+ "tags": ["Functions"],
3300
+ "responses": {
3301
+ "200": {
3302
+ "description": "Function routing",
3303
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
3304
+ "type": "object",
3305
+ "properties": { "data": { "oneOf": [{ "$ref": "#/components/schemas/FunctionRouting" }, { "type": "null" }] } }
3306
+ }] } } }
3307
+ },
3308
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3309
+ "404": { "$ref": "#/components/responses/NotFound" }
3310
+ }
3311
+ }
3312
+ },
3313
+ "/functions/{id}/route": {
3314
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3315
+ "put": {
3316
+ "operationId": "setFunctionRoute",
3317
+ "summary": "Bind a route to a function",
3318
+ "description": "Binds inbound mail to this function. The route target is either\na specific verified domain (scoped) or the org's fallback (any\nactive domain with no scoped binding). If another function is\nalready bound at the target, returns a `conflict` envelope\ndescribing the holder; re-issue with `takeover: true` to\ndeactivate that prior binding and install this one.\n",
3319
+ "tags": ["Functions"],
3320
+ "requestBody": {
3321
+ "required": true,
3322
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FunctionRouteBody" } } }
3323
+ },
3324
+ "responses": {
3325
+ "200": {
3326
+ "description": "Route bound, or conflict requiring takeover",
3327
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
3328
+ "type": "object",
3329
+ "properties": { "data": { "$ref": "#/components/schemas/FunctionRouteResult" } }
3330
+ }] } } }
3331
+ },
3332
+ "400": { "$ref": "#/components/responses/ValidationError" },
3333
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3334
+ "404": { "$ref": "#/components/responses/NotFound" }
3335
+ }
3336
+ },
3337
+ "delete": {
3338
+ "operationId": "unsetFunctionRoute",
3339
+ "summary": "Unbind any route from a function",
3340
+ "description": "Deactivates every active endpoint bound to this function. The\nfunction stays deployed but stops receiving inbound mail. Safe\nto call when no route is currently bound (no-op).\n",
3341
+ "tags": ["Functions"],
3342
+ "responses": {
3343
+ "200": {
3344
+ "description": "Route unbound",
3345
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
3346
+ "type": "object",
3347
+ "properties": { "data": {
3348
+ "type": "object",
3349
+ "properties": { "unrouted": {
3350
+ "type": "boolean",
3351
+ "enum": [true]
3352
+ } },
3353
+ "required": ["unrouted"]
3354
+ } }
3355
+ }] } } }
3356
+ },
3357
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3358
+ "404": { "$ref": "#/components/responses/NotFound" }
3359
+ }
3360
+ }
3361
+ },
3196
3362
  "/functions/{id}/secrets": {
3197
3363
  "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3198
3364
  "get": {
@@ -6718,6 +6884,178 @@ const openapiDocument = {
6718
6884
  "trace_url"
6719
6885
  ]
6720
6886
  },
6887
+ "FunctionRouting": {
6888
+ "type": "object",
6889
+ "description": "A single route binding for a function. `domain` is null when the\nbinding is the org's fallback (any active domain without a scoped\nbinding); otherwise it carries the scoped domain. `rules` is\nreserved for future routing predicates.\n",
6890
+ "properties": {
6891
+ "endpoint_id": {
6892
+ "type": "string",
6893
+ "format": "uuid"
6894
+ },
6895
+ "enabled": { "type": "boolean" },
6896
+ "domain": {
6897
+ "type": ["object", "null"],
6898
+ "properties": {
6899
+ "id": {
6900
+ "type": "string",
6901
+ "format": "uuid"
6902
+ },
6903
+ "name": { "type": ["string", "null"] }
6904
+ },
6905
+ "required": ["id"]
6906
+ },
6907
+ "rules": {
6908
+ "type": "object",
6909
+ "description": "Future routing predicates. Currently empty."
6910
+ },
6911
+ "delivery_count": { "type": "integer" },
6912
+ "success_count": { "type": "integer" },
6913
+ "failure_count": { "type": "integer" },
6914
+ "consecutive_fails": { "type": "integer" },
6915
+ "last_delivery_at": {
6916
+ "type": ["string", "null"],
6917
+ "format": "date-time"
6918
+ },
6919
+ "last_success_at": {
6920
+ "type": ["string", "null"],
6921
+ "format": "date-time"
6922
+ },
6923
+ "last_failure_at": {
6924
+ "type": ["string", "null"],
6925
+ "format": "date-time"
6926
+ }
6927
+ },
6928
+ "required": [
6929
+ "endpoint_id",
6930
+ "enabled",
6931
+ "domain",
6932
+ "rules"
6933
+ ]
6934
+ },
6935
+ "RoutingTopology": {
6936
+ "type": "object",
6937
+ "description": "Org-wide map of function routing: which domain points at which\nfunction, the org's fallback binding (if any), and every\ndeployed function with no route currently bound.\n",
6938
+ "properties": {
6939
+ "domains": {
6940
+ "type": "array",
6941
+ "items": {
6942
+ "type": "object",
6943
+ "properties": {
6944
+ "domain_id": {
6945
+ "type": "string",
6946
+ "format": "uuid"
6947
+ },
6948
+ "domain": { "type": "string" },
6949
+ "routed_function": {
6950
+ "type": ["object", "null"],
6951
+ "properties": {
6952
+ "id": {
6953
+ "type": "string",
6954
+ "format": "uuid"
6955
+ },
6956
+ "name": { "type": "string" }
6957
+ },
6958
+ "required": ["id", "name"]
6959
+ },
6960
+ "endpoint_enabled": { "type": ["boolean", "null"] }
6961
+ },
6962
+ "required": [
6963
+ "domain_id",
6964
+ "domain",
6965
+ "routed_function",
6966
+ "endpoint_enabled"
6967
+ ]
6968
+ }
6969
+ },
6970
+ "fallback_function": {
6971
+ "type": ["object", "null"],
6972
+ "properties": {
6973
+ "id": {
6974
+ "type": "string",
6975
+ "format": "uuid"
6976
+ },
6977
+ "name": { "type": "string" }
6978
+ },
6979
+ "required": ["id", "name"]
6980
+ },
6981
+ "fallback_enabled": { "type": ["boolean", "null"] },
6982
+ "unrouted_functions": {
6983
+ "type": "array",
6984
+ "items": {
6985
+ "type": "object",
6986
+ "properties": {
6987
+ "id": {
6988
+ "type": "string",
6989
+ "format": "uuid"
6990
+ },
6991
+ "name": { "type": "string" }
6992
+ },
6993
+ "required": ["id", "name"]
6994
+ }
6995
+ }
6996
+ },
6997
+ "required": [
6998
+ "domains",
6999
+ "fallback_function",
7000
+ "fallback_enabled",
7001
+ "unrouted_functions"
7002
+ ]
7003
+ },
7004
+ "FunctionRouteBody": {
7005
+ "type": "object",
7006
+ "description": "Target for a route binding. Either a specific verified domain\n(scoped) or the org-wide fallback. Pass `takeover: true` to\ndeactivate any conflicting binding before installing this one.\n",
7007
+ "properties": {
7008
+ "target": { "oneOf": [{
7009
+ "type": "object",
7010
+ "properties": {
7011
+ "kind": {
7012
+ "type": "string",
7013
+ "enum": ["domain"]
7014
+ },
7015
+ "domainId": {
7016
+ "type": "string",
7017
+ "format": "uuid"
7018
+ }
7019
+ },
7020
+ "required": ["kind", "domainId"]
7021
+ }, {
7022
+ "type": "object",
7023
+ "properties": { "kind": {
7024
+ "type": "string",
7025
+ "enum": ["fallback"]
7026
+ } },
7027
+ "required": ["kind"]
7028
+ }] },
7029
+ "takeover": {
7030
+ "type": "boolean",
7031
+ "description": "When true, deactivate any conflicting binding before installing this one."
7032
+ }
7033
+ },
7034
+ "required": ["target"]
7035
+ },
7036
+ "FunctionRouteResult": {
7037
+ "type": "object",
7038
+ "description": "On success, carries the new `routing`. On conflict, carries\n`conflict` describing the binding holder so the caller can\nre-issue with `takeover: true`.\n",
7039
+ "properties": {
7040
+ "routing": { "oneOf": [{ "$ref": "#/components/schemas/FunctionRouting" }, { "type": "null" }] },
7041
+ "conflict": {
7042
+ "type": "object",
7043
+ "properties": {
7044
+ "kind": {
7045
+ "type": "string",
7046
+ "enum": ["http", "function"]
7047
+ },
7048
+ "functionId": {
7049
+ "type": ["string", "null"],
7050
+ "format": "uuid"
7051
+ },
7052
+ "functionName": { "type": ["string", "null"] },
7053
+ "url": { "type": ["string", "null"] }
7054
+ },
7055
+ "required": ["kind"]
7056
+ }
7057
+ }
7058
+ },
6721
7059
  "FunctionTestRunState": {
6722
7060
  "type": "string",
6723
7061
  "description": "High-level state for a function test run trace:\n - `send_failed`: the initial test email send failed.\n - `waiting_for_send`: the test run was created but no send result has been recorded yet.\n - `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.\n - `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.\n - `completed`: the function webhook completed successfully.\n - `failed`: webhook delivery exhausted retries.\n",
@@ -10433,7 +10771,7 @@ const operationManifest = [
10433
10771
  "binaryResponse": false,
10434
10772
  "bodyRequired": true,
10435
10773
  "command": "create-function",
10436
- "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). Primitive signs\neach delivery and forwards the `Primitive-Signature` header to\nthe handler. Verify the raw request body with\n`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification\nthe request body parses to an `email.received` event (see\n`EmailReceivedEvent` and the Webhook payload section for the full\nschema). Code is bundled before being uploaded; ship a single\nself-contained file rather than relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the internal runtime URL is not returned by\nthe API and is not a customer-facing integration surface.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,\n`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
10774
+ "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). Primitive signs\neach delivery and forwards the `Primitive-Signature` header to\nthe handler. Verify the raw request body with\n`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification\nthe request body parses to an `email.received` event (see\n`EmailReceivedEvent` and the Webhook payload section for the full\nschema). Code is bundled before being uploaded; ship a single\nself-contained file rather than relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Routing.** On successful deploy, the function code is live\nin the runtime, but inbound mail will not reach it until at\nleast one route is bound. Routes are managed from the Primitive\ndashboard. A `deploy_status` of `deployed` means the script is\ninstalled, not that the function is receiving mail. The\ninternal runtime URL is not returned by the API and is not a\ncustomer-facing integration surface.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,\n`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
10437
10775
  "hasJsonBody": true,
10438
10776
  "method": "POST",
10439
10777
  "operationId": "createFunction",
@@ -10569,7 +10907,7 @@ const operationManifest = [
10569
10907
  "binaryResponse": false,
10570
10908
  "bodyRequired": false,
10571
10909
  "command": "delete-function",
10572
- "description": "Soft-deletes the function row, removes the script from the edge\nruntime, and deactivates the auto-wired webhook endpoint so no\nfurther inbound mail is delivered. Past deploy history,\ninvocations, and logs are retained.\n\nReturns 502 if the runtime delete fails partway; the function\nrow stays in place and the call is safe to retry until it\nsucceeds.\n",
10910
+ "description": "Soft-deletes the function row, removes the script from the edge\nruntime, and deactivates any route bound to this function so no\nfurther inbound mail is delivered. Past deploy history,\ninvocations, and logs are retained.\n\nReturns 502 if the runtime delete fails partway; the function\nrow stays in place and the call is safe to retry until it\nsucceeds.\n",
10573
10911
  "hasJsonBody": false,
10574
10912
  "method": "DELETE",
10575
10913
  "operationId": "deleteFunction",
@@ -10693,62 +11031,133 @@ const operationManifest = [
10693
11031
  {
10694
11032
  "binaryResponse": false,
10695
11033
  "bodyRequired": false,
10696
- "command": "get-function-test-run-trace",
10697
- "description": "Returns the current end-to-end trace for a function test run.\nThe trace is intentionally partial while the test is still in\nflight: callers can poll this endpoint and watch it fill in\nfrom send -> inbound -> webhook deliveries -> outbound\nrequests, logs, and replies.\n",
11034
+ "command": "get-function-routing",
11035
+ "description": "Returns the endpoint binding for the function, or null when no\nroute is currently bound. The binding identifies whether the\nfunction receives mail for a specific domain (scoped) or for any\nactive domain that has no scoped binding (fallback).\n",
10698
11036
  "hasJsonBody": false,
10699
11037
  "method": "GET",
10700
- "operationId": "getFunctionTestRunTrace",
10701
- "path": "/functions/{id}/test-runs/{run_id}/trace",
11038
+ "operationId": "getFunctionRouting",
11039
+ "path": "/functions/{id}/routing",
10702
11040
  "pathParams": [{
10703
11041
  "description": "Resource UUID",
10704
11042
  "enum": null,
10705
11043
  "name": "id",
10706
11044
  "required": true,
10707
11045
  "type": "string"
10708
- }, {
10709
- "description": "Function test run id returned by POST /functions/{id}/test.",
10710
- "enum": null,
10711
- "name": "run_id",
10712
- "required": true,
10713
- "type": "string"
10714
11046
  }],
10715
11047
  "queryParams": [],
10716
11048
  "requestSchema": null,
10717
- "responseSchema": {
11049
+ "responseSchema": { "oneOf": [{
10718
11050
  "type": "object",
10719
- "description": "End-to-end trace for a `POST /functions/{id}/test` run. The\nshape is stable, but many nested sections are null or empty\nuntil the corresponding phase has happened.\n",
11051
+ "description": "A single route binding for a function. `domain` is null when the\nbinding is the org's fallback (any active domain without a scoped\nbinding); otherwise it carries the scoped domain. `rules` is\nreserved for future routing predicates.\n",
10720
11052
  "properties": {
10721
- "state": {
11053
+ "endpoint_id": {
10722
11054
  "type": "string",
10723
- "description": "High-level state for a function test run trace:\n - `send_failed`: the initial test email send failed.\n - `waiting_for_send`: the test run was created but no send result has been recorded yet.\n - `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.\n - `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.\n - `completed`: the function webhook completed successfully.\n - `failed`: webhook delivery exhausted retries.\n",
10724
- "enum": [
10725
- "send_failed",
10726
- "waiting_for_send",
10727
- "waiting_for_inbound",
10728
- "waiting_for_function",
10729
- "completed",
10730
- "failed"
10731
- ]
11055
+ "format": "uuid"
10732
11056
  },
10733
- "test_run": {
10734
- "type": "object",
11057
+ "enabled": { "type": "boolean" },
11058
+ "domain": {
11059
+ "type": ["object", "null"],
10735
11060
  "properties": {
10736
11061
  "id": {
10737
11062
  "type": "string",
10738
11063
  "format": "uuid"
10739
11064
  },
10740
- "function_id": {
10741
- "type": "string",
10742
- "format": "uuid"
10743
- },
10744
- "inbound_domain": { "type": "string" },
10745
- "to": { "type": "string" },
10746
- "from": { "type": "string" },
10747
- "subject": { "type": "string" },
10748
- "poll_since": {
10749
- "type": "string",
10750
- "format": "date-time"
10751
- },
11065
+ "name": { "type": ["string", "null"] }
11066
+ },
11067
+ "required": ["id"]
11068
+ },
11069
+ "rules": {
11070
+ "type": "object",
11071
+ "description": "Future routing predicates. Currently empty."
11072
+ },
11073
+ "delivery_count": { "type": "integer" },
11074
+ "success_count": { "type": "integer" },
11075
+ "failure_count": { "type": "integer" },
11076
+ "consecutive_fails": { "type": "integer" },
11077
+ "last_delivery_at": {
11078
+ "type": ["string", "null"],
11079
+ "format": "date-time"
11080
+ },
11081
+ "last_success_at": {
11082
+ "type": ["string", "null"],
11083
+ "format": "date-time"
11084
+ },
11085
+ "last_failure_at": {
11086
+ "type": ["string", "null"],
11087
+ "format": "date-time"
11088
+ }
11089
+ },
11090
+ "required": [
11091
+ "endpoint_id",
11092
+ "enabled",
11093
+ "domain",
11094
+ "rules"
11095
+ ]
11096
+ }, { "type": "null" }] },
11097
+ "sdkName": "getFunctionRouting",
11098
+ "summary": "Get a function's current route binding",
11099
+ "tag": "Functions",
11100
+ "tagCommand": "functions"
11101
+ },
11102
+ {
11103
+ "binaryResponse": false,
11104
+ "bodyRequired": false,
11105
+ "command": "get-function-test-run-trace",
11106
+ "description": "Returns the current end-to-end trace for a function test run.\nThe trace is intentionally partial while the test is still in\nflight: callers can poll this endpoint and watch it fill in\nfrom send -> inbound -> webhook deliveries -> outbound\nrequests, logs, and replies.\n",
11107
+ "hasJsonBody": false,
11108
+ "method": "GET",
11109
+ "operationId": "getFunctionTestRunTrace",
11110
+ "path": "/functions/{id}/test-runs/{run_id}/trace",
11111
+ "pathParams": [{
11112
+ "description": "Resource UUID",
11113
+ "enum": null,
11114
+ "name": "id",
11115
+ "required": true,
11116
+ "type": "string"
11117
+ }, {
11118
+ "description": "Function test run id returned by POST /functions/{id}/test.",
11119
+ "enum": null,
11120
+ "name": "run_id",
11121
+ "required": true,
11122
+ "type": "string"
11123
+ }],
11124
+ "queryParams": [],
11125
+ "requestSchema": null,
11126
+ "responseSchema": {
11127
+ "type": "object",
11128
+ "description": "End-to-end trace for a `POST /functions/{id}/test` run. The\nshape is stable, but many nested sections are null or empty\nuntil the corresponding phase has happened.\n",
11129
+ "properties": {
11130
+ "state": {
11131
+ "type": "string",
11132
+ "description": "High-level state for a function test run trace:\n - `send_failed`: the initial test email send failed.\n - `waiting_for_send`: the test run was created but no send result has been recorded yet.\n - `waiting_for_inbound`: the test send was queued and the matching inbound email has not arrived yet.\n - `waiting_for_function`: the inbound email arrived and webhook/function processing is still in flight.\n - `completed`: the function webhook completed successfully.\n - `failed`: webhook delivery exhausted retries.\n",
11133
+ "enum": [
11134
+ "send_failed",
11135
+ "waiting_for_send",
11136
+ "waiting_for_inbound",
11137
+ "waiting_for_function",
11138
+ "completed",
11139
+ "failed"
11140
+ ]
11141
+ },
11142
+ "test_run": {
11143
+ "type": "object",
11144
+ "properties": {
11145
+ "id": {
11146
+ "type": "string",
11147
+ "format": "uuid"
11148
+ },
11149
+ "function_id": {
11150
+ "type": "string",
11151
+ "format": "uuid"
11152
+ },
11153
+ "inbound_domain": { "type": "string" },
11154
+ "to": { "type": "string" },
11155
+ "from": { "type": "string" },
11156
+ "subject": { "type": "string" },
11157
+ "poll_since": {
11158
+ "type": "string",
11159
+ "format": "date-time"
11160
+ },
10752
11161
  "created_at": {
10753
11162
  "type": "string",
10754
11163
  "format": "date-time"
@@ -11123,6 +11532,92 @@ const operationManifest = [
11123
11532
  "tag": "Functions",
11124
11533
  "tagCommand": "functions"
11125
11534
  },
11535
+ {
11536
+ "binaryResponse": false,
11537
+ "bodyRequired": false,
11538
+ "command": "get-org-routing-topology",
11539
+ "description": "Returns a single snapshot of how inbound mail is routed across\nthis org's active domains and functions: which active domain has\nwhich function bound, the org's fallback function (if any), and\nevery deployed function with no route bound. Use this to answer\n\"which of my functions actually receive mail?\" diagnostically.\n",
11540
+ "hasJsonBody": false,
11541
+ "method": "GET",
11542
+ "operationId": "getOrgRoutingTopology",
11543
+ "path": "/functions/routing-topology",
11544
+ "pathParams": [],
11545
+ "queryParams": [],
11546
+ "requestSchema": null,
11547
+ "responseSchema": {
11548
+ "type": "object",
11549
+ "description": "Org-wide map of function routing: which domain points at which\nfunction, the org's fallback binding (if any), and every\ndeployed function with no route currently bound.\n",
11550
+ "properties": {
11551
+ "domains": {
11552
+ "type": "array",
11553
+ "items": {
11554
+ "type": "object",
11555
+ "properties": {
11556
+ "domain_id": {
11557
+ "type": "string",
11558
+ "format": "uuid"
11559
+ },
11560
+ "domain": { "type": "string" },
11561
+ "routed_function": {
11562
+ "type": ["object", "null"],
11563
+ "properties": {
11564
+ "id": {
11565
+ "type": "string",
11566
+ "format": "uuid"
11567
+ },
11568
+ "name": { "type": "string" }
11569
+ },
11570
+ "required": ["id", "name"]
11571
+ },
11572
+ "endpoint_enabled": { "type": ["boolean", "null"] }
11573
+ },
11574
+ "required": [
11575
+ "domain_id",
11576
+ "domain",
11577
+ "routed_function",
11578
+ "endpoint_enabled"
11579
+ ]
11580
+ }
11581
+ },
11582
+ "fallback_function": {
11583
+ "type": ["object", "null"],
11584
+ "properties": {
11585
+ "id": {
11586
+ "type": "string",
11587
+ "format": "uuid"
11588
+ },
11589
+ "name": { "type": "string" }
11590
+ },
11591
+ "required": ["id", "name"]
11592
+ },
11593
+ "fallback_enabled": { "type": ["boolean", "null"] },
11594
+ "unrouted_functions": {
11595
+ "type": "array",
11596
+ "items": {
11597
+ "type": "object",
11598
+ "properties": {
11599
+ "id": {
11600
+ "type": "string",
11601
+ "format": "uuid"
11602
+ },
11603
+ "name": { "type": "string" }
11604
+ },
11605
+ "required": ["id", "name"]
11606
+ }
11607
+ }
11608
+ },
11609
+ "required": [
11610
+ "domains",
11611
+ "fallback_function",
11612
+ "fallback_enabled",
11613
+ "unrouted_functions"
11614
+ ]
11615
+ },
11616
+ "sdkName": "getOrgRoutingTopology",
11617
+ "summary": "Get the org's function routing topology",
11618
+ "tag": "Functions",
11619
+ "tagCommand": "functions"
11620
+ },
11126
11621
  {
11127
11622
  "binaryResponse": false,
11128
11623
  "bodyRequired": false,
@@ -11342,6 +11837,130 @@ const operationManifest = [
11342
11837
  "tag": "Functions",
11343
11838
  "tagCommand": "functions"
11344
11839
  },
11840
+ {
11841
+ "binaryResponse": false,
11842
+ "bodyRequired": true,
11843
+ "command": "set-function-route",
11844
+ "description": "Binds inbound mail to this function. The route target is either\na specific verified domain (scoped) or the org's fallback (any\nactive domain with no scoped binding). If another function is\nalready bound at the target, returns a `conflict` envelope\ndescribing the holder; re-issue with `takeover: true` to\ndeactivate that prior binding and install this one.\n",
11845
+ "hasJsonBody": true,
11846
+ "method": "PUT",
11847
+ "operationId": "setFunctionRoute",
11848
+ "path": "/functions/{id}/route",
11849
+ "pathParams": [{
11850
+ "description": "Resource UUID",
11851
+ "enum": null,
11852
+ "name": "id",
11853
+ "required": true,
11854
+ "type": "string"
11855
+ }],
11856
+ "queryParams": [],
11857
+ "requestSchema": {
11858
+ "type": "object",
11859
+ "description": "Target for a route binding. Either a specific verified domain\n(scoped) or the org-wide fallback. Pass `takeover: true` to\ndeactivate any conflicting binding before installing this one.\n",
11860
+ "properties": {
11861
+ "target": { "oneOf": [{
11862
+ "type": "object",
11863
+ "properties": {
11864
+ "kind": {
11865
+ "type": "string",
11866
+ "enum": ["domain"]
11867
+ },
11868
+ "domainId": {
11869
+ "type": "string",
11870
+ "format": "uuid"
11871
+ }
11872
+ },
11873
+ "required": ["kind", "domainId"]
11874
+ }, {
11875
+ "type": "object",
11876
+ "properties": { "kind": {
11877
+ "type": "string",
11878
+ "enum": ["fallback"]
11879
+ } },
11880
+ "required": ["kind"]
11881
+ }] },
11882
+ "takeover": {
11883
+ "type": "boolean",
11884
+ "description": "When true, deactivate any conflicting binding before installing this one."
11885
+ }
11886
+ },
11887
+ "required": ["target"]
11888
+ },
11889
+ "responseSchema": {
11890
+ "type": "object",
11891
+ "description": "On success, carries the new `routing`. On conflict, carries\n`conflict` describing the binding holder so the caller can\nre-issue with `takeover: true`.\n",
11892
+ "properties": {
11893
+ "routing": { "oneOf": [{
11894
+ "type": "object",
11895
+ "description": "A single route binding for a function. `domain` is null when the\nbinding is the org's fallback (any active domain without a scoped\nbinding); otherwise it carries the scoped domain. `rules` is\nreserved for future routing predicates.\n",
11896
+ "properties": {
11897
+ "endpoint_id": {
11898
+ "type": "string",
11899
+ "format": "uuid"
11900
+ },
11901
+ "enabled": { "type": "boolean" },
11902
+ "domain": {
11903
+ "type": ["object", "null"],
11904
+ "properties": {
11905
+ "id": {
11906
+ "type": "string",
11907
+ "format": "uuid"
11908
+ },
11909
+ "name": { "type": ["string", "null"] }
11910
+ },
11911
+ "required": ["id"]
11912
+ },
11913
+ "rules": {
11914
+ "type": "object",
11915
+ "description": "Future routing predicates. Currently empty."
11916
+ },
11917
+ "delivery_count": { "type": "integer" },
11918
+ "success_count": { "type": "integer" },
11919
+ "failure_count": { "type": "integer" },
11920
+ "consecutive_fails": { "type": "integer" },
11921
+ "last_delivery_at": {
11922
+ "type": ["string", "null"],
11923
+ "format": "date-time"
11924
+ },
11925
+ "last_success_at": {
11926
+ "type": ["string", "null"],
11927
+ "format": "date-time"
11928
+ },
11929
+ "last_failure_at": {
11930
+ "type": ["string", "null"],
11931
+ "format": "date-time"
11932
+ }
11933
+ },
11934
+ "required": [
11935
+ "endpoint_id",
11936
+ "enabled",
11937
+ "domain",
11938
+ "rules"
11939
+ ]
11940
+ }, { "type": "null" }] },
11941
+ "conflict": {
11942
+ "type": "object",
11943
+ "properties": {
11944
+ "kind": {
11945
+ "type": "string",
11946
+ "enum": ["http", "function"]
11947
+ },
11948
+ "functionId": {
11949
+ "type": ["string", "null"],
11950
+ "format": "uuid"
11951
+ },
11952
+ "functionName": { "type": ["string", "null"] },
11953
+ "url": { "type": ["string", "null"] }
11954
+ },
11955
+ "required": ["kind"]
11956
+ }
11957
+ }
11958
+ },
11959
+ "sdkName": "setFunctionRoute",
11960
+ "summary": "Bind a route to a function",
11961
+ "tag": "Functions",
11962
+ "tagCommand": "functions"
11963
+ },
11345
11964
  {
11346
11965
  "binaryResponse": false,
11347
11966
  "bodyRequired": true,
@@ -11495,6 +12114,37 @@ const operationManifest = [
11495
12114
  "tag": "Functions",
11496
12115
  "tagCommand": "functions"
11497
12116
  },
12117
+ {
12118
+ "binaryResponse": false,
12119
+ "bodyRequired": false,
12120
+ "command": "unset-function-route",
12121
+ "description": "Deactivates every active endpoint bound to this function. The\nfunction stays deployed but stops receiving inbound mail. Safe\nto call when no route is currently bound (no-op).\n",
12122
+ "hasJsonBody": false,
12123
+ "method": "DELETE",
12124
+ "operationId": "unsetFunctionRoute",
12125
+ "path": "/functions/{id}/route",
12126
+ "pathParams": [{
12127
+ "description": "Resource UUID",
12128
+ "enum": null,
12129
+ "name": "id",
12130
+ "required": true,
12131
+ "type": "string"
12132
+ }],
12133
+ "queryParams": [],
12134
+ "requestSchema": null,
12135
+ "responseSchema": {
12136
+ "type": "object",
12137
+ "properties": { "unrouted": {
12138
+ "type": "boolean",
12139
+ "enum": [true]
12140
+ } },
12141
+ "required": ["unrouted"]
12142
+ },
12143
+ "sdkName": "unsetFunctionRoute",
12144
+ "summary": "Unbind any route from a function",
12145
+ "tag": "Functions",
12146
+ "tagCommand": "functions"
12147
+ },
11498
12148
  {
11499
12149
  "binaryResponse": false,
11500
12150
  "bodyRequired": true,
@@ -13415,6 +14065,10 @@ async function createAuthenticatedCliApiClient(params) {
13415
14065
  apiBaseUrl: requestConfig.apiBaseUrl,
13416
14066
  configDir: params.configDir
13417
14067
  });
14068
+ if (auth.apiKey === void 0) {
14069
+ const hint = detectPrimitiveKeyEnvMisname(params.env ?? process.env);
14070
+ if (hint) process.stderr.write(`${hint}\n`);
14071
+ }
13418
14072
  if (auth.source === "stored" && auth.credentials) {
13419
14073
  const refreshed = await refreshStoredCliCredentials({
13420
14074
  apiBaseUrl: auth.apiBaseUrl,
@@ -15374,9 +16028,7 @@ function checkApiKey(opts) {
15374
16028
  message: "provided but does not start with prim_",
15375
16029
  hint: "Verify the key is a Primitive API key, not a value from another service."
15376
16030
  };
15377
- const primitiveKey = env.PRIMITIVE_KEY;
15378
- const primitiveApiKey = env.PRIMITIVE_API_KEY;
15379
- if ((primitiveKey?.length ?? 0) > 0 && (primitiveApiKey?.length ?? 0) === 0) return {
16031
+ if (detectPrimitiveKeyEnvMisname(env)) return {
15380
16032
  status: "fail",
15381
16033
  message: "PRIMITIVE_KEY is set but the CLI reads PRIMITIVE_API_KEY",
15382
16034
  hint: "Rename your env var, or re-run with PRIMITIVE_API_KEY=$PRIMITIVE_KEY."
@@ -16603,6 +17255,18 @@ const SECRET_SOURCE_FLAGS_DESCRIPTION = "Safe sources: --secret-from-env KEY rea
16603
17255
  const SINGLE_SECRET_VALUE_SOURCE_DESCRIPTION = "Instead of --value, use --value-from-env ENV_VAR, --value-file PATH, --value-from-env-file FILE[:KEY], or --stdin to avoid putting the secret value in shell history or process argv. If KEY is omitted from --value-from-env-file, the command's --key is used.";
16604
17256
  //#endregion
16605
17257
  //#region src/oclif/commands/functions-deploy.ts
17258
+ async function writeRouteStatusHint(apiClient, functionId) {
17259
+ try {
17260
+ const result = await getFunctionRouting({
17261
+ client: apiClient.client,
17262
+ path: { id: functionId },
17263
+ responseStyle: "fields"
17264
+ });
17265
+ if (result.error) return;
17266
+ if (result.data?.data) process.stderr.write("Route bound. Function will receive inbound mail.\n");
17267
+ else process.stderr.write(`Deployed but no route is bound. Inbound mail will not reach this function until you bind one: primitive functions route-set --id ${functionId} --domain <domain-id> (or --fallback)\n`);
17268
+ } catch {}
17269
+ }
16606
17270
  async function runDeployWithSecrets(api, params) {
16607
17271
  const createResult = await api.createFunction({
16608
17272
  code: params.code,
@@ -16902,10 +17566,11 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16902
17566
  const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
16903
17567
  process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
16904
17568
  process.exitCode = 1;
16905
- }
17569
+ } else await writeRouteStatusHint(apiClient, payload.id);
16906
17570
  return;
16907
17571
  }
16908
17572
  this.log(JSON.stringify(payload, null, 2));
17573
+ await writeRouteStatusHint(apiClient, payload.id);
16909
17574
  });
16910
17575
  }
16911
17576
  async runSourceMode(flags, sourceDir) {
@@ -16997,10 +17662,11 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16997
17662
  const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
16998
17663
  process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
16999
17664
  process.exitCode = 1;
17000
- }
17665
+ } else await writeRouteStatusHint(apiClient, payload.id);
17001
17666
  return;
17002
17667
  }
17003
17668
  this.log(JSON.stringify(payload, null, 2));
17669
+ await writeRouteStatusHint(apiClient, payload.id);
17004
17670
  }
17005
17671
  };
17006
17672
  //#endregion
@@ -17011,8 +17677,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
17011
17677
  name: "Primitive Team",
17012
17678
  url: "https://primitive.dev"
17013
17679
  };
17014
- const SDK_VERSION_RANGE = "^0.35.1";
17015
- const CLI_VERSION_RANGE = "^0.35.1";
17680
+ const SDK_VERSION_RANGE = "^0.36.0";
17681
+ const CLI_VERSION_RANGE = "^0.36.0";
17016
17682
  const ESBUILD_VERSION_RANGE = "^0.27.0";
17017
17683
  function renderHandler() {
17018
17684
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -17319,6 +17985,23 @@ After the first deploy, copy the returned function id into your shell:
17319
17985
  export PRIMITIVE_FUNCTION_ID=<fn-id>
17320
17986
  \`\`\`
17321
17987
 
17988
+ ## Bind a route
17989
+
17990
+ A deployed Function does not receive inbound mail until a route is
17991
+ bound to it. Without this step, the Function is installed in the
17992
+ runtime but unreachable, and \`functions test\` will refuse to send
17993
+ (returns 422 \`no_endpoint\`).
17994
+
17995
+ \`\`\`
17996
+ primitive functions route-set --id "$PRIMITIVE_FUNCTION_ID" --domain <domain-id>
17997
+ \`\`\`
17998
+
17999
+ Use \`--fallback\` instead of \`--domain\` to bind the Function as the
18000
+ org-wide fallback for any active domain that has no scoped binding.
18001
+ Run \`primitive functions route-get --id "$PRIMITIVE_FUNCTION_ID"\` to
18002
+ inspect the current binding, or \`primitive functions routing-topology\`
18003
+ for the org-wide view of which domain points at which Function.
18004
+
17322
18005
  ## Prove it works
17323
18006
 
17324
18007
  \`\`\`
@@ -17929,6 +18612,239 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
17929
18612
  }
17930
18613
  };
17931
18614
  //#endregion
18615
+ //#region src/oclif/commands/functions-route-get.ts
18616
+ var FunctionsRouteGetCommand = class FunctionsRouteGetCommand extends Command {
18617
+ static description = `Show the current route binding for a function. Returns the binding (domain or fallback, with delivery counters) or null when no route is bound.`;
18618
+ static summary = "Show a function's current route binding";
18619
+ static examples = ["<%= config.bin %> functions route-get --id <fn-id>"];
18620
+ static flags = {
18621
+ "api-key": Flags.string({
18622
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18623
+ env: "PRIMITIVE_API_KEY"
18624
+ }),
18625
+ "api-base-url": Flags.string({
18626
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18627
+ env: "PRIMITIVE_API_BASE_URL",
18628
+ hidden: true
18629
+ }),
18630
+ id: Flags.string({
18631
+ description: "Function id (UUID) whose route binding to show.",
18632
+ required: true
18633
+ }),
18634
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18635
+ };
18636
+ async run() {
18637
+ const { flags } = await this.parse(FunctionsRouteGetCommand);
18638
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18639
+ apiKey: flags["api-key"],
18640
+ apiBaseUrl: flags["api-base-url"],
18641
+ configDir: this.config.configDir
18642
+ });
18643
+ await runWithTiming(flags.time, async () => {
18644
+ const result = await getFunctionRouting({
18645
+ client: apiClient.client,
18646
+ path: { id: flags.id },
18647
+ responseStyle: "fields"
18648
+ });
18649
+ if (result.error) {
18650
+ const payload = extractErrorPayload(result.error);
18651
+ writeErrorWithHints(payload);
18652
+ surfaceUnauthorizedHint({
18653
+ auth,
18654
+ baseUrlOverridden,
18655
+ configDir: this.config.configDir,
18656
+ payload
18657
+ });
18658
+ process.exitCode = 1;
18659
+ return;
18660
+ }
18661
+ this.log(JSON.stringify(result.data.data, null, 2));
18662
+ });
18663
+ }
18664
+ };
18665
+ //#endregion
18666
+ //#region src/oclif/commands/functions-route-set.ts
18667
+ var FunctionsRouteSetCommand = class FunctionsRouteSetCommand extends Command {
18668
+ static description = `Bind inbound mail to a function by setting its route target.
18669
+
18670
+ Exactly one of --domain or --fallback is required. --domain scopes the
18671
+ binding to a single verified inbound domain. --fallback binds the
18672
+ function to any active domain that has no scoped binding of its own.
18673
+
18674
+ If another function is already bound at the target, the API returns a
18675
+ conflict envelope rather than overwriting; re-run with --takeover to
18676
+ deactivate the prior binding before installing this one.`;
18677
+ static summary = "Bind inbound mail to a function";
18678
+ static examples = [
18679
+ "<%= config.bin %> functions route-set --id <fn-id> --domain <domain-id>",
18680
+ "<%= config.bin %> functions route-set --id <fn-id> --fallback",
18681
+ "<%= config.bin %> functions route-set --id <fn-id> --domain <domain-id> --takeover"
18682
+ ];
18683
+ static flags = {
18684
+ "api-key": Flags.string({
18685
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18686
+ env: "PRIMITIVE_API_KEY"
18687
+ }),
18688
+ "api-base-url": Flags.string({
18689
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18690
+ env: "PRIMITIVE_API_BASE_URL",
18691
+ hidden: true
18692
+ }),
18693
+ id: Flags.string({
18694
+ description: "Function id (UUID) to bind a route to.",
18695
+ required: true
18696
+ }),
18697
+ domain: Flags.string({
18698
+ description: "Verified inbound domain id (UUID) to scope this function to. Mutually exclusive with --fallback.",
18699
+ exclusive: ["fallback"]
18700
+ }),
18701
+ fallback: Flags.boolean({
18702
+ description: "Bind this function as the org fallback (any active domain without a scoped binding). Mutually exclusive with --domain.",
18703
+ exclusive: ["domain"]
18704
+ }),
18705
+ takeover: Flags.boolean({ description: "Deactivate any conflicting binding before installing this one. Without this flag, the API returns a `conflict` envelope when another function is already bound at the target." }),
18706
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18707
+ };
18708
+ async run() {
18709
+ const { flags } = await this.parse(FunctionsRouteSetCommand);
18710
+ if (!flags.domain && !flags.fallback) {
18711
+ process.stderr.write("Provide exactly one of --domain (scoped binding) or --fallback (org fallback).\n");
18712
+ process.exitCode = 1;
18713
+ return;
18714
+ }
18715
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18716
+ apiKey: flags["api-key"],
18717
+ apiBaseUrl: flags["api-base-url"],
18718
+ configDir: this.config.configDir
18719
+ });
18720
+ await runWithTiming(flags.time, async () => {
18721
+ const target = flags.domain ? {
18722
+ kind: "domain",
18723
+ domainId: flags.domain
18724
+ } : { kind: "fallback" };
18725
+ const result = await setFunctionRoute({
18726
+ client: apiClient.client,
18727
+ path: { id: flags.id },
18728
+ body: {
18729
+ target,
18730
+ ...flags.takeover ? { takeover: true } : {}
18731
+ },
18732
+ responseStyle: "fields"
18733
+ });
18734
+ if (result.error) {
18735
+ const payload = extractErrorPayload(result.error);
18736
+ writeErrorWithHints(payload);
18737
+ surfaceUnauthorizedHint({
18738
+ auth,
18739
+ baseUrlOverridden,
18740
+ configDir: this.config.configDir,
18741
+ payload
18742
+ });
18743
+ process.exitCode = 1;
18744
+ return;
18745
+ }
18746
+ this.log(JSON.stringify(result.data.data, null, 2));
18747
+ });
18748
+ }
18749
+ };
18750
+ //#endregion
18751
+ //#region src/oclif/commands/functions-route-unset.ts
18752
+ var FunctionsRouteUnsetCommand = class FunctionsRouteUnsetCommand extends Command {
18753
+ static description = `Unbind every active route from a function. The function stays deployed but stops receiving inbound mail. Safe to call when no route is currently bound.`;
18754
+ static summary = "Unbind any route from a function";
18755
+ static examples = ["<%= config.bin %> functions route-unset --id <fn-id>"];
18756
+ static flags = {
18757
+ "api-key": Flags.string({
18758
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18759
+ env: "PRIMITIVE_API_KEY"
18760
+ }),
18761
+ "api-base-url": Flags.string({
18762
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18763
+ env: "PRIMITIVE_API_BASE_URL",
18764
+ hidden: true
18765
+ }),
18766
+ id: Flags.string({
18767
+ description: "Function id (UUID) whose routes should be unbound.",
18768
+ required: true
18769
+ }),
18770
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18771
+ };
18772
+ async run() {
18773
+ const { flags } = await this.parse(FunctionsRouteUnsetCommand);
18774
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18775
+ apiKey: flags["api-key"],
18776
+ apiBaseUrl: flags["api-base-url"],
18777
+ configDir: this.config.configDir
18778
+ });
18779
+ await runWithTiming(flags.time, async () => {
18780
+ const result = await unsetFunctionRoute({
18781
+ client: apiClient.client,
18782
+ path: { id: flags.id },
18783
+ responseStyle: "fields"
18784
+ });
18785
+ if (result.error) {
18786
+ const payload = extractErrorPayload(result.error);
18787
+ writeErrorWithHints(payload);
18788
+ surfaceUnauthorizedHint({
18789
+ auth,
18790
+ baseUrlOverridden,
18791
+ configDir: this.config.configDir,
18792
+ payload
18793
+ });
18794
+ process.exitCode = 1;
18795
+ return;
18796
+ }
18797
+ this.log(JSON.stringify(result.data.data, null, 2));
18798
+ });
18799
+ }
18800
+ };
18801
+ //#endregion
18802
+ //#region src/oclif/commands/functions-routing-topology.ts
18803
+ var FunctionsRoutingTopologyCommand = class FunctionsRoutingTopologyCommand extends Command {
18804
+ static description = `Show the org-wide function routing topology. Lists every active domain with its bound function (if any), the org fallback function (if any), and every deployed function with no route currently bound.`;
18805
+ static summary = "Show the org-wide function routing topology";
18806
+ static examples = ["<%= config.bin %> functions routing-topology"];
18807
+ static flags = {
18808
+ "api-key": Flags.string({
18809
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18810
+ env: "PRIMITIVE_API_KEY"
18811
+ }),
18812
+ "api-base-url": Flags.string({
18813
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18814
+ env: "PRIMITIVE_API_BASE_URL",
18815
+ hidden: true
18816
+ }),
18817
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18818
+ };
18819
+ async run() {
18820
+ const { flags } = await this.parse(FunctionsRoutingTopologyCommand);
18821
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18822
+ apiKey: flags["api-key"],
18823
+ apiBaseUrl: flags["api-base-url"],
18824
+ configDir: this.config.configDir
18825
+ });
18826
+ await runWithTiming(flags.time, async () => {
18827
+ const result = await getOrgRoutingTopology({
18828
+ client: apiClient.client,
18829
+ responseStyle: "fields"
18830
+ });
18831
+ if (result.error) {
18832
+ const payload = extractErrorPayload(result.error);
18833
+ writeErrorWithHints(payload);
18834
+ surfaceUnauthorizedHint({
18835
+ auth,
18836
+ baseUrlOverridden,
18837
+ configDir: this.config.configDir,
18838
+ payload
18839
+ });
18840
+ process.exitCode = 1;
18841
+ return;
18842
+ }
18843
+ this.log(JSON.stringify(result.data.data, null, 2));
18844
+ });
18845
+ }
18846
+ };
18847
+ //#endregion
17932
18848
  //#region src/oclif/commands/functions-set-secret.ts
17933
18849
  async function runSetSecret(api, params) {
17934
18850
  const setResult = await api.setSecret({
@@ -18106,6 +19022,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
18106
19022
  return;
18107
19023
  }
18108
19024
  this.log(JSON.stringify(outcome.result, null, 2));
19025
+ if (outcome.result.redeploy === void 0) process.stderr.write(`Secret ${flags.key} saved. Not live until redeploy. Re-run with --redeploy, or run \`primitive functions redeploy --id ${flags.id} --file <bundle.js>\`.\n`);
18109
19026
  });
18110
19027
  }
18111
19028
  };
@@ -18230,7 +19147,7 @@ async function maybeWriteEndpointNoiseWarning(params) {
18230
19147
  } catch {}
18231
19148
  }
18232
19149
  var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Command {
18233
- static description = "Send a real test email through MX to trigger this function. With --wait, blocks until the function has processed the inbound; with --show-sends, also prints any outbound sends the function emitted in response.";
19150
+ static description = "Send a real test email through MX to trigger this function. The function must have an active route bound (see `functions route-set`); without one the API returns a 422 immediately so no doomed test send is queued. With --wait, blocks until the function has processed the inbound; with --show-sends, also prints any outbound sends the function emitted in response.";
18234
19151
  static summary = "Trigger a test invocation; with --wait, watch it land";
18235
19152
  static examples = [
18236
19153
  "<%= config.bin %> functions test --id <fn-id>",
@@ -18336,13 +19253,35 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18336
19253
  return;
18337
19254
  }
18338
19255
  const fetched = result.data.data;
19256
+ if (fetched.inbound_email && Array.isArray(fetched.deliveries) && fetched.deliveries.length === 0 && Date.now() - startedAt > 15e3) {
19257
+ writeFunctionTestProgress(`Inbound email arrived but no route matched. Bind one with: primitive functions route-set --id ${flags.id} --domain <domain-id> (or --fallback), then retry.`);
19258
+ process.exitCode = 1;
19259
+ return;
19260
+ }
18339
19261
  if (TERMINAL_TEST_TRACE_STATES.has(fetched.state)) {
18340
19262
  trace = fetched;
18341
19263
  break;
18342
19264
  }
18343
19265
  await sleep$1(pollIntervalMs);
18344
19266
  }
18345
- if (!trace) this.error(`Timed out after ${flags.timeout}s waiting for function test run ${invocation.test_run_id} to complete. Browse ${invocation.watch_url} for the live view, or inspect ${invocation.trace_url}.`, { exit: 2 });
19267
+ if (!trace) {
19268
+ const finalTrace = ((await getFunctionTestRunTrace({
19269
+ client: apiClient.client,
19270
+ path: {
19271
+ id: flags.id,
19272
+ run_id: invocation.test_run_id
19273
+ },
19274
+ responseStyle: "fields"
19275
+ }).catch(() => null))?.data)?.data;
19276
+ const inboundLanded = Boolean(finalTrace?.inbound_email);
19277
+ const deliveryCount = finalTrace?.deliveries?.length ?? 0;
19278
+ const logCount = finalTrace?.logs?.length ?? 0;
19279
+ const replyCount = finalTrace?.replies?.length ?? 0;
19280
+ const webhookStatus = finalTrace?.inbound_email?.webhook_status ?? "n/a";
19281
+ writeFunctionTestProgress(`Timed out after ${flags.timeout}s. Trace summary: inbound_landed=${inboundLanded} deliveries=${deliveryCount} logs=${logCount} replies=${replyCount} webhook_status=${webhookStatus}. Browse ${invocation.watch_url} for the live view, or inspect ${invocation.trace_url}.`);
19282
+ process.exitCode = 2;
19283
+ return;
19284
+ }
18346
19285
  const outcome = buildFunctionTestOutcome({
18347
19286
  elapsedSeconds: Math.round((Date.now() - startedAt) / 1e3),
18348
19287
  functionId: flags.id,
@@ -20944,6 +21883,10 @@ const COMMANDS = {
20944
21883
  "functions:set-secret": FunctionsSetSecretCommand,
20945
21884
  "functions:test": FunctionsTestFunctionCommand,
20946
21885
  "functions:test-function": FunctionsTestFunctionCommand,
21886
+ "functions:route-set": FunctionsRouteSetCommand,
21887
+ "functions:route-unset": FunctionsRouteUnsetCommand,
21888
+ "functions:route-get": FunctionsRouteGetCommand,
21889
+ "functions:routing-topology": FunctionsRoutingTopologyCommand,
20947
21890
  ...Object.fromEntries(Object.entries(CANONICAL_OPERATION_ALIASES).map(([alias, target]) => {
20948
21891
  const command = generatedCommands[target];
20949
21892
  if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
@@ -1,4 +1,4 @@
1
- import { b as normalizeApiBaseUrl, c as resolveConfigEnvironment, i as loadCliConfig } from "../cli-config-DREZ2BxT.js";
1
+ import { c as resolveConfigEnvironment, i as loadCliConfig, x as normalizeApiBaseUrl } from "../cli-config-D7wN_PBc.js";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.35.1",
3
+ "version": "0.36.0",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,