@primitivedotdev/cli 0.35.1 → 0.37.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.
@@ -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,
@@ -13810,8 +14464,11 @@ const RESERVED_FLAG_NAMES = new Set([
13810
14464
  "envelope",
13811
14465
  "output"
13812
14466
  ]);
13813
- function bodyFieldFlag(field) {
13814
- const common = { description: field.description || field.name };
14467
+ function bodyFieldFlag(field, aliases) {
14468
+ const common = {
14469
+ description: field.description || field.name,
14470
+ ...aliases && aliases.length > 0 ? { aliases } : {}
14471
+ };
13815
14472
  if (field.kind === "boolean") return Flags.boolean({
13816
14473
  ...common,
13817
14474
  allowNo: true
@@ -13853,12 +14510,13 @@ function buildFlags(operation) {
13853
14510
  flags["raw-body"] = Flags.string({ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available." });
13854
14511
  flags["body-file"] = Flags.string({ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload." });
13855
14512
  const bodyFields = extractBodyFields(operation.requestSchema);
14513
+ const aliasesForOperation = OPERATION_FLAG_ALIASES[operation.sdkName];
13856
14514
  for (const field of bodyFields) {
13857
14515
  if (field.kind === "complex") continue;
13858
14516
  const name = flagName(field.name);
13859
14517
  if (RESERVED_FLAG_NAMES.has(name)) continue;
13860
14518
  if (flags[name] !== void 0) continue;
13861
- flags[name] = bodyFieldFlag(field);
14519
+ flags[name] = bodyFieldFlag(field, aliasesForOperation?.[field.name]);
13862
14520
  bodyFieldFlagToProperty.set(name, field.name);
13863
14521
  }
13864
14522
  }
@@ -13907,8 +14565,11 @@ const OPERATION_HINTS = {
13907
14565
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
13908
14566
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
13909
14567
  createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
13910
- setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON."
14568
+ setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
14569
+ startAgentSignup: "Tip: also pass --signup-code <code> (request from Primitive; invite-only during the agent beta) and --terms-accepted. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
14570
+ verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
13911
14571
  };
14572
+ const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
13912
14573
  function createOperationCommand(operation) {
13913
14574
  const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
13914
14575
  const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
@@ -15374,9 +16035,7 @@ function checkApiKey(opts) {
15374
16035
  message: "provided but does not start with prim_",
15375
16036
  hint: "Verify the key is a Primitive API key, not a value from another service."
15376
16037
  };
15377
- const primitiveKey = env.PRIMITIVE_KEY;
15378
- const primitiveApiKey = env.PRIMITIVE_API_KEY;
15379
- if ((primitiveKey?.length ?? 0) > 0 && (primitiveApiKey?.length ?? 0) === 0) return {
16038
+ if (detectPrimitiveKeyEnvMisname(env)) return {
15380
16039
  status: "fail",
15381
16040
  message: "PRIMITIVE_KEY is set but the CLI reads PRIMITIVE_API_KEY",
15382
16041
  hint: "Rename your env var, or re-run with PRIMITIVE_API_KEY=$PRIMITIVE_KEY."
@@ -16603,6 +17262,18 @@ const SECRET_SOURCE_FLAGS_DESCRIPTION = "Safe sources: --secret-from-env KEY rea
16603
17262
  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
17263
  //#endregion
16605
17264
  //#region src/oclif/commands/functions-deploy.ts
17265
+ async function writeRouteStatusHint(apiClient, functionId) {
17266
+ try {
17267
+ const result = await getFunctionRouting({
17268
+ client: apiClient.client,
17269
+ path: { id: functionId },
17270
+ responseStyle: "fields"
17271
+ });
17272
+ if (result.error) return;
17273
+ if (result.data?.data) process.stderr.write("Route bound. Function will receive inbound mail.\n");
17274
+ 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`);
17275
+ } catch {}
17276
+ }
16606
17277
  async function runDeployWithSecrets(api, params) {
16607
17278
  const createResult = await api.createFunction({
16608
17279
  code: params.code,
@@ -16902,10 +17573,11 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16902
17573
  const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
16903
17574
  process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
16904
17575
  process.exitCode = 1;
16905
- }
17576
+ } else await writeRouteStatusHint(apiClient, payload.id);
16906
17577
  return;
16907
17578
  }
16908
17579
  this.log(JSON.stringify(payload, null, 2));
17580
+ await writeRouteStatusHint(apiClient, payload.id);
16909
17581
  });
16910
17582
  }
16911
17583
  async runSourceMode(flags, sourceDir) {
@@ -16997,10 +17669,11 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
16997
17669
  const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
16998
17670
  process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
16999
17671
  process.exitCode = 1;
17000
- }
17672
+ } else await writeRouteStatusHint(apiClient, payload.id);
17001
17673
  return;
17002
17674
  }
17003
17675
  this.log(JSON.stringify(payload, null, 2));
17676
+ await writeRouteStatusHint(apiClient, payload.id);
17004
17677
  }
17005
17678
  };
17006
17679
  //#endregion
@@ -17011,8 +17684,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
17011
17684
  name: "Primitive Team",
17012
17685
  url: "https://primitive.dev"
17013
17686
  };
17014
- const SDK_VERSION_RANGE = "^0.35.1";
17015
- const CLI_VERSION_RANGE = "^0.35.1";
17687
+ const SDK_VERSION_RANGE = "^0.37.0";
17688
+ const CLI_VERSION_RANGE = "^0.37.0";
17016
17689
  const ESBUILD_VERSION_RANGE = "^0.27.0";
17017
17690
  function renderHandler() {
17018
17691
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -17087,22 +17760,50 @@ function inboundRecipientDomains(event: EmailReceivedEvent): Set<string> {
17087
17760
  // SMTP recipients instead.
17088
17761
  //
17089
17762
  // The default check skips:
17763
+ // - bounce notifications, which RFC 5321 requires to use an empty
17764
+ // SMTP envelope sender (MAIL FROM:<>). Replying to a null sender
17765
+ // is forbidden and would itself bounce, producing a
17766
+ // bounce-of-bounce chain. The body header on a bounce typically
17767
+ // reads "From: MAILER-DAEMON@..." which a naive From-only check
17768
+ // would treat as a normal sender, so we gate on the envelope here.
17090
17769
  // - direct self-mail where From equals one of the inbound recipients;
17091
- // - mailer-daemon/postmaster bounces from the same domain as the inbound;
17770
+ // - mailer-daemon/postmaster bounces from the same domain as the
17771
+ // inbound, as a backup for bounces that arrive with a non-empty
17772
+ // envelope sender;
17092
17773
  // - any address explicitly listed in EXTRA_SELF_ADDRESSES.
17093
17774
  //
17094
- // Extend this helper if you need stricter detection. Common additions:
17095
- // - Honor RFC 3834 auto-response headers: skip when
17096
- // event.email.headers["auto-submitted"] is anything other than "no",
17097
- // or when a List-Unsubscribe / Precedence: bulk header is present.
17775
+ // Anything with no identifiable sender at all (envelope + From both
17776
+ // empty) is treated as a loop terminator: better to drop one ambiguous
17777
+ // message than to reply blindly and loop on a bounce.
17778
+ //
17779
+ // Extend this helper if you need stricter detection. Common additions
17780
+ // not implemented here today:
17781
+ // - Honor RFC 3834 auto-response headers: skip when an
17782
+ // auto-submitted header is anything other than "no", or when a
17783
+ // list-unsubscribe / precedence: bulk header is present. The
17784
+ // EmailReceivedEvent.email.headers shape does not currently surface
17785
+ // these, so detection requires either a parsed-headers field on
17786
+ // the event or a parse of the raw RFC 822 body.
17098
17787
  // - Track Message-ID / In-Reply-To chains to break ping-pong loops
17099
17788
  // between two cooperating handlers on different domains.
17789
+ // - Rate-limit replies per sender per hour as a safety net.
17100
17790
  export function isLoop(event: EmailReceivedEvent): boolean {
17791
+ // RFC 5321: bounce notifications use the null MAIL FROM (envelope
17792
+ // sender = empty string). Some MTAs report this as "<>" verbatim.
17793
+ // Treat either as an unambiguous bounce signal.
17794
+ const envelopeSender = (event.email.smtp.mail_from || "").trim();
17795
+ if (envelopeSender === "" || envelopeSender === "<>") return true;
17796
+
17101
17797
  const fromAddresses = [
17102
17798
  ...extractEmailAddresses(event.email.headers.from),
17103
17799
  ...extractEmailAddresses(event.email.smtp.mail_from),
17104
17800
  ];
17105
- if (fromAddresses.length === 0) return false;
17801
+ // No identifiable sender across either envelope or header: treat as
17802
+ // a loop terminator. Was return false in the original template; that
17803
+ // returned mail with empty headers straight back into the handler
17804
+ // and let bounces with malformed bodies slip past the bounce guard
17805
+ // above.
17806
+ if (fromAddresses.length === 0) return true;
17106
17807
 
17107
17808
  const inboundAddresses = new Set(inboundRecipientAddresses(event));
17108
17809
  const inboundDomains = inboundRecipientDomains(event);
@@ -17319,6 +18020,23 @@ After the first deploy, copy the returned function id into your shell:
17319
18020
  export PRIMITIVE_FUNCTION_ID=<fn-id>
17320
18021
  \`\`\`
17321
18022
 
18023
+ ## Bind a route
18024
+
18025
+ A deployed Function does not receive inbound mail until a route is
18026
+ bound to it. Without this step, the Function is installed in the
18027
+ runtime but unreachable, and \`functions test\` will refuse to send
18028
+ (returns 422 \`no_endpoint\`).
18029
+
18030
+ \`\`\`
18031
+ primitive functions route-set --id "$PRIMITIVE_FUNCTION_ID" --domain <domain-id>
18032
+ \`\`\`
18033
+
18034
+ Use \`--fallback\` instead of \`--domain\` to bind the Function as the
18035
+ org-wide fallback for any active domain that has no scoped binding.
18036
+ Run \`primitive functions route-get --id "$PRIMITIVE_FUNCTION_ID"\` to
18037
+ inspect the current binding, or \`primitive functions routing-topology\`
18038
+ for the org-wide view of which domain points at which Function.
18039
+
17322
18040
  ## Prove it works
17323
18041
 
17324
18042
  \`\`\`
@@ -17561,6 +18279,25 @@ function emitLogRows(rows, jsonl) {
17561
18279
  process.stdout.write(`${line}\n`);
17562
18280
  }
17563
18281
  }
18282
+ async function readFunctionInvocations(client, id) {
18283
+ try {
18284
+ const result = await getFunction({
18285
+ client,
18286
+ path: { id },
18287
+ responseStyle: "fields",
18288
+ signal: AbortSignal.timeout(3e3)
18289
+ });
18290
+ if (result.error) return null;
18291
+ const fn = result.data?.data;
18292
+ if (!fn) return null;
18293
+ return {
18294
+ invocations_total: typeof fn.invocations_total === "number" ? fn.invocations_total : 0,
18295
+ invocations_24h: typeof fn.invocations_24h === "number" ? fn.invocations_24h : 0
18296
+ };
18297
+ } catch {
18298
+ return null;
18299
+ }
18300
+ }
17564
18301
  var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
17565
18302
  static description = "List or follow function execution logs. Defaults to compact text output; use --jsonl for one JSON object per log row.";
17566
18303
  static summary = "List or follow a function's execution logs";
@@ -17659,7 +18396,14 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
17659
18396
  cursor = page.next_cursor;
17660
18397
  }
17661
18398
  if (rows.length === 0 && !wroteEmptyHint) {
17662
- process.stderr.write(flags.follow ? hasObservedLogs ? "Waiting for new function logs...\n" : "No function logs yet. Waiting for new rows...\n" : "No function logs yet. Trigger the function, then run this command again.\n");
18399
+ let emptyHint;
18400
+ if (flags.follow) emptyHint = hasObservedLogs ? "Waiting for new function logs...\n" : "No function logs yet. Waiting for new rows...\n";
18401
+ else if (flags.cursor) emptyHint = "No more function logs after this cursor.\n";
18402
+ else {
18403
+ const fnInvocations = await readFunctionInvocations(apiClient.client, flags.id);
18404
+ emptyHint = fnInvocations && fnInvocations.invocations_total > 0 ? `No function logs yet, but this function has been invoked ${fnInvocations.invocations_total} time(s) (${fnInvocations.invocations_24h} in the last 24h). Your handler likely has no console.log/console.error calls on the path that fired. Add logging and redeploy to surface details.\n` : "No function logs yet. Trigger the function, then run this command again.\n";
18405
+ }
18406
+ process.stderr.write(emptyHint);
17663
18407
  wroteEmptyHint = true;
17664
18408
  }
17665
18409
  emitLogRows(rows, flags.jsonl);
@@ -17929,6 +18673,239 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
17929
18673
  }
17930
18674
  };
17931
18675
  //#endregion
18676
+ //#region src/oclif/commands/functions-route-get.ts
18677
+ var FunctionsRouteGetCommand = class FunctionsRouteGetCommand extends Command {
18678
+ 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.`;
18679
+ static summary = "Show a function's current route binding";
18680
+ static examples = ["<%= config.bin %> functions route-get --id <fn-id>"];
18681
+ static flags = {
18682
+ "api-key": Flags.string({
18683
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18684
+ env: "PRIMITIVE_API_KEY"
18685
+ }),
18686
+ "api-base-url": Flags.string({
18687
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18688
+ env: "PRIMITIVE_API_BASE_URL",
18689
+ hidden: true
18690
+ }),
18691
+ id: Flags.string({
18692
+ description: "Function id (UUID) whose route binding to show.",
18693
+ required: true
18694
+ }),
18695
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18696
+ };
18697
+ async run() {
18698
+ const { flags } = await this.parse(FunctionsRouteGetCommand);
18699
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18700
+ apiKey: flags["api-key"],
18701
+ apiBaseUrl: flags["api-base-url"],
18702
+ configDir: this.config.configDir
18703
+ });
18704
+ await runWithTiming(flags.time, async () => {
18705
+ const result = await getFunctionRouting({
18706
+ client: apiClient.client,
18707
+ path: { id: flags.id },
18708
+ responseStyle: "fields"
18709
+ });
18710
+ if (result.error) {
18711
+ const payload = extractErrorPayload(result.error);
18712
+ writeErrorWithHints(payload);
18713
+ surfaceUnauthorizedHint({
18714
+ auth,
18715
+ baseUrlOverridden,
18716
+ configDir: this.config.configDir,
18717
+ payload
18718
+ });
18719
+ process.exitCode = 1;
18720
+ return;
18721
+ }
18722
+ this.log(JSON.stringify(result.data.data, null, 2));
18723
+ });
18724
+ }
18725
+ };
18726
+ //#endregion
18727
+ //#region src/oclif/commands/functions-route-set.ts
18728
+ var FunctionsRouteSetCommand = class FunctionsRouteSetCommand extends Command {
18729
+ static description = `Bind inbound mail to a function by setting its route target.
18730
+
18731
+ Exactly one of --domain or --fallback is required. --domain scopes the
18732
+ binding to a single verified inbound domain. --fallback binds the
18733
+ function to any active domain that has no scoped binding of its own.
18734
+
18735
+ If another function is already bound at the target, the API returns a
18736
+ conflict envelope rather than overwriting; re-run with --takeover to
18737
+ deactivate the prior binding before installing this one.`;
18738
+ static summary = "Bind inbound mail to a function";
18739
+ static examples = [
18740
+ "<%= config.bin %> functions route-set --id <fn-id> --domain <domain-id>",
18741
+ "<%= config.bin %> functions route-set --id <fn-id> --fallback",
18742
+ "<%= config.bin %> functions route-set --id <fn-id> --domain <domain-id> --takeover"
18743
+ ];
18744
+ static flags = {
18745
+ "api-key": Flags.string({
18746
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18747
+ env: "PRIMITIVE_API_KEY"
18748
+ }),
18749
+ "api-base-url": Flags.string({
18750
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18751
+ env: "PRIMITIVE_API_BASE_URL",
18752
+ hidden: true
18753
+ }),
18754
+ id: Flags.string({
18755
+ description: "Function id (UUID) to bind a route to.",
18756
+ required: true
18757
+ }),
18758
+ domain: Flags.string({
18759
+ description: "Verified inbound domain id (UUID) to scope this function to. Mutually exclusive with --fallback.",
18760
+ exclusive: ["fallback"]
18761
+ }),
18762
+ fallback: Flags.boolean({
18763
+ description: "Bind this function as the org fallback (any active domain without a scoped binding). Mutually exclusive with --domain.",
18764
+ exclusive: ["domain"]
18765
+ }),
18766
+ 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." }),
18767
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18768
+ };
18769
+ async run() {
18770
+ const { flags } = await this.parse(FunctionsRouteSetCommand);
18771
+ if (!flags.domain && !flags.fallback) {
18772
+ process.stderr.write("Provide exactly one of --domain (scoped binding) or --fallback (org fallback).\n");
18773
+ process.exitCode = 1;
18774
+ return;
18775
+ }
18776
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18777
+ apiKey: flags["api-key"],
18778
+ apiBaseUrl: flags["api-base-url"],
18779
+ configDir: this.config.configDir
18780
+ });
18781
+ await runWithTiming(flags.time, async () => {
18782
+ const target = flags.domain ? {
18783
+ kind: "domain",
18784
+ domainId: flags.domain
18785
+ } : { kind: "fallback" };
18786
+ const result = await setFunctionRoute({
18787
+ client: apiClient.client,
18788
+ path: { id: flags.id },
18789
+ body: {
18790
+ target,
18791
+ ...flags.takeover ? { takeover: true } : {}
18792
+ },
18793
+ responseStyle: "fields"
18794
+ });
18795
+ if (result.error) {
18796
+ const payload = extractErrorPayload(result.error);
18797
+ writeErrorWithHints(payload);
18798
+ surfaceUnauthorizedHint({
18799
+ auth,
18800
+ baseUrlOverridden,
18801
+ configDir: this.config.configDir,
18802
+ payload
18803
+ });
18804
+ process.exitCode = 1;
18805
+ return;
18806
+ }
18807
+ this.log(JSON.stringify(result.data.data, null, 2));
18808
+ });
18809
+ }
18810
+ };
18811
+ //#endregion
18812
+ //#region src/oclif/commands/functions-route-unset.ts
18813
+ var FunctionsRouteUnsetCommand = class FunctionsRouteUnsetCommand extends Command {
18814
+ 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.`;
18815
+ static summary = "Unbind any route from a function";
18816
+ static examples = ["<%= config.bin %> functions route-unset --id <fn-id>"];
18817
+ static flags = {
18818
+ "api-key": Flags.string({
18819
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18820
+ env: "PRIMITIVE_API_KEY"
18821
+ }),
18822
+ "api-base-url": Flags.string({
18823
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18824
+ env: "PRIMITIVE_API_BASE_URL",
18825
+ hidden: true
18826
+ }),
18827
+ id: Flags.string({
18828
+ description: "Function id (UUID) whose routes should be unbound.",
18829
+ required: true
18830
+ }),
18831
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18832
+ };
18833
+ async run() {
18834
+ const { flags } = await this.parse(FunctionsRouteUnsetCommand);
18835
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18836
+ apiKey: flags["api-key"],
18837
+ apiBaseUrl: flags["api-base-url"],
18838
+ configDir: this.config.configDir
18839
+ });
18840
+ await runWithTiming(flags.time, async () => {
18841
+ const result = await unsetFunctionRoute({
18842
+ client: apiClient.client,
18843
+ path: { id: flags.id },
18844
+ responseStyle: "fields"
18845
+ });
18846
+ if (result.error) {
18847
+ const payload = extractErrorPayload(result.error);
18848
+ writeErrorWithHints(payload);
18849
+ surfaceUnauthorizedHint({
18850
+ auth,
18851
+ baseUrlOverridden,
18852
+ configDir: this.config.configDir,
18853
+ payload
18854
+ });
18855
+ process.exitCode = 1;
18856
+ return;
18857
+ }
18858
+ this.log(JSON.stringify(result.data.data, null, 2));
18859
+ });
18860
+ }
18861
+ };
18862
+ //#endregion
18863
+ //#region src/oclif/commands/functions-routing-topology.ts
18864
+ var FunctionsRoutingTopologyCommand = class FunctionsRoutingTopologyCommand extends Command {
18865
+ 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.`;
18866
+ static summary = "Show the org-wide function routing topology";
18867
+ static examples = ["<%= config.bin %> functions routing-topology"];
18868
+ static flags = {
18869
+ "api-key": Flags.string({
18870
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18871
+ env: "PRIMITIVE_API_KEY"
18872
+ }),
18873
+ "api-base-url": Flags.string({
18874
+ description: API_BASE_URL_FLAG_DESCRIPTION,
18875
+ env: "PRIMITIVE_API_BASE_URL",
18876
+ hidden: true
18877
+ }),
18878
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18879
+ };
18880
+ async run() {
18881
+ const { flags } = await this.parse(FunctionsRoutingTopologyCommand);
18882
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18883
+ apiKey: flags["api-key"],
18884
+ apiBaseUrl: flags["api-base-url"],
18885
+ configDir: this.config.configDir
18886
+ });
18887
+ await runWithTiming(flags.time, async () => {
18888
+ const result = await getOrgRoutingTopology({
18889
+ client: apiClient.client,
18890
+ responseStyle: "fields"
18891
+ });
18892
+ if (result.error) {
18893
+ const payload = extractErrorPayload(result.error);
18894
+ writeErrorWithHints(payload);
18895
+ surfaceUnauthorizedHint({
18896
+ auth,
18897
+ baseUrlOverridden,
18898
+ configDir: this.config.configDir,
18899
+ payload
18900
+ });
18901
+ process.exitCode = 1;
18902
+ return;
18903
+ }
18904
+ this.log(JSON.stringify(result.data.data, null, 2));
18905
+ });
18906
+ }
18907
+ };
18908
+ //#endregion
17932
18909
  //#region src/oclif/commands/functions-set-secret.ts
17933
18910
  async function runSetSecret(api, params) {
17934
18911
  const setResult = await api.setSecret({
@@ -18106,6 +19083,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
18106
19083
  return;
18107
19084
  }
18108
19085
  this.log(JSON.stringify(outcome.result, null, 2));
19086
+ 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
19087
  });
18110
19088
  }
18111
19089
  };
@@ -18230,7 +19208,7 @@ async function maybeWriteEndpointNoiseWarning(params) {
18230
19208
  } catch {}
18231
19209
  }
18232
19210
  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.";
19211
+ 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
19212
  static summary = "Trigger a test invocation; with --wait, watch it land";
18235
19213
  static examples = [
18236
19214
  "<%= config.bin %> functions test --id <fn-id>",
@@ -18336,13 +19314,35 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
18336
19314
  return;
18337
19315
  }
18338
19316
  const fetched = result.data.data;
19317
+ if (fetched.inbound_email && Array.isArray(fetched.deliveries) && fetched.deliveries.length === 0 && Date.now() - startedAt > 15e3) {
19318
+ 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.`);
19319
+ process.exitCode = 1;
19320
+ return;
19321
+ }
18339
19322
  if (TERMINAL_TEST_TRACE_STATES.has(fetched.state)) {
18340
19323
  trace = fetched;
18341
19324
  break;
18342
19325
  }
18343
19326
  await sleep$1(pollIntervalMs);
18344
19327
  }
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 });
19328
+ if (!trace) {
19329
+ const finalTrace = ((await getFunctionTestRunTrace({
19330
+ client: apiClient.client,
19331
+ path: {
19332
+ id: flags.id,
19333
+ run_id: invocation.test_run_id
19334
+ },
19335
+ responseStyle: "fields"
19336
+ }).catch(() => null))?.data)?.data;
19337
+ const inboundLanded = Boolean(finalTrace?.inbound_email);
19338
+ const deliveryCount = finalTrace?.deliveries?.length ?? 0;
19339
+ const logCount = finalTrace?.logs?.length ?? 0;
19340
+ const replyCount = finalTrace?.replies?.length ?? 0;
19341
+ const webhookStatus = finalTrace?.inbound_email?.webhook_status ?? "n/a";
19342
+ 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}.`);
19343
+ process.exitCode = 2;
19344
+ return;
19345
+ }
18346
19346
  const outcome = buildFunctionTestOutcome({
18347
19347
  elapsedSeconds: Math.round((Date.now() - startedAt) / 1e3),
18348
19348
  functionId: flags.id,
@@ -20565,12 +21565,14 @@ var OtpResendCommand = class extends SigninOtpResendCommand {
20565
21565
  };
20566
21566
  //#endregion
20567
21567
  //#region src/oclif/commands/whoami.ts
20568
- function formatWhoamiSummary(account) {
20569
- return [
21568
+ function formatWhoamiSummary(account, managedInboxDomain) {
21569
+ const lines = [
20570
21570
  `Authenticated as ${account.email}`,
20571
21571
  `Account id: ${account.id}`,
20572
21572
  `Plan: ${account.plan}`
20573
- ].join("\n");
21573
+ ];
21574
+ if (managedInboxDomain) lines.push(`Managed inbox: any-local-part@${managedInboxDomain}`);
21575
+ return lines.join("\n");
20574
21576
  }
20575
21577
  var WhoamiCommand = class WhoamiCommand extends Command {
20576
21578
  static description = `Print the account currently authenticated by saved OAuth credentials or an explicit API key. Useful as a credentials smoke test: confirms auth is live and shows which account it belongs to.
@@ -20624,11 +21626,22 @@ var WhoamiCommand = class WhoamiCommand extends Command {
20624
21626
  process.stderr.write("Server returned an empty account body; this should not happen for a valid key.\n");
20625
21627
  throw new Errors.CLIError("unexpected empty response");
20626
21628
  }
21629
+ let managedInboxDomain = null;
21630
+ try {
21631
+ const domainsResult = await listDomains({
21632
+ client: apiClient.client,
21633
+ responseStyle: "fields"
21634
+ });
21635
+ if (!domainsResult.error) managedInboxDomain = (domainsResult.data?.data ?? []).find((row) => row.verified && row.managed_zone !== null)?.domain ?? null;
21636
+ } catch {}
20627
21637
  if (flags.json) {
20628
- this.log(JSON.stringify(account, null, 2));
21638
+ this.log(JSON.stringify({
21639
+ ...account,
21640
+ managed_inbox_domain: managedInboxDomain
21641
+ }, null, 2));
20629
21642
  return;
20630
21643
  }
20631
- this.log(formatWhoamiSummary(account));
21644
+ this.log(formatWhoamiSummary(account, managedInboxDomain));
20632
21645
  });
20633
21646
  }
20634
21647
  };
@@ -20944,6 +21957,10 @@ const COMMANDS = {
20944
21957
  "functions:set-secret": FunctionsSetSecretCommand,
20945
21958
  "functions:test": FunctionsTestFunctionCommand,
20946
21959
  "functions:test-function": FunctionsTestFunctionCommand,
21960
+ "functions:route-set": FunctionsRouteSetCommand,
21961
+ "functions:route-unset": FunctionsRouteUnsetCommand,
21962
+ "functions:route-get": FunctionsRouteGetCommand,
21963
+ "functions:routing-topology": FunctionsRoutingTopologyCommand,
20947
21964
  ...Object.fromEntries(Object.entries(CANONICAL_OPERATION_ALIASES).map(([alias, target]) => {
20948
21965
  const command = generatedCommands[target];
20949
21966
  if (!command) throw new Error(`Missing generated command target for alias ${alias}`);