@primitivedotdev/cli 0.26.4 → 0.29.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.
- package/README.md +24 -4
- package/dist/oclif/index.js +2629 -212
- package/package.json +12 -6
package/dist/oclif/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { hostname } from "node:os";
|
|
7
|
+
import process$1 from "node:process";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
7
9
|
//#region \0rolldown/runtime.js
|
|
8
10
|
var __defProp = Object.defineProperty;
|
|
9
11
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -647,6 +649,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
|
|
|
647
649
|
getAccount: () => getAccount,
|
|
648
650
|
getEmail: () => getEmail,
|
|
649
651
|
getFunction: () => getFunction,
|
|
652
|
+
getFunctionTestRunTrace: () => getFunctionTestRunTrace,
|
|
650
653
|
getSendPermissions: () => getSendPermissions,
|
|
651
654
|
getSentEmail: () => getSentEmail,
|
|
652
655
|
getStorageStats: () => getStorageStats,
|
|
@@ -664,11 +667,13 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
|
|
|
664
667
|
replayDelivery: () => replayDelivery,
|
|
665
668
|
replayEmailWebhooks: () => replayEmailWebhooks,
|
|
666
669
|
replyToEmail: () => replyToEmail,
|
|
670
|
+
resendCliSignupVerification: () => resendCliSignupVerification,
|
|
667
671
|
rotateWebhookSecret: () => rotateWebhookSecret,
|
|
668
672
|
searchEmails: () => searchEmails,
|
|
669
673
|
sendEmail: () => sendEmail,
|
|
670
674
|
setFunctionSecret: () => setFunctionSecret,
|
|
671
675
|
startCliLogin: () => startCliLogin,
|
|
676
|
+
startCliSignup: () => startCliSignup,
|
|
672
677
|
testEndpoint: () => testEndpoint,
|
|
673
678
|
testFunction: () => testFunction,
|
|
674
679
|
updateAccount: () => updateAccount,
|
|
@@ -676,6 +681,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
|
|
|
676
681
|
updateEndpoint: () => updateEndpoint,
|
|
677
682
|
updateFilter: () => updateFilter,
|
|
678
683
|
updateFunction: () => updateFunction,
|
|
684
|
+
verifyCliSignup: () => verifyCliSignup,
|
|
679
685
|
verifyDomain: () => verifyDomain
|
|
680
686
|
});
|
|
681
687
|
/**
|
|
@@ -711,6 +717,54 @@ const pollCliLogin = (options) => (options.client ?? client).post({
|
|
|
711
717
|
}
|
|
712
718
|
});
|
|
713
719
|
/**
|
|
720
|
+
* Start CLI account signup
|
|
721
|
+
*
|
|
722
|
+
* Starts a terminal-native CLI signup. The API validates the signup code,
|
|
723
|
+
* creates a pending signup session, sends an email verification code, and
|
|
724
|
+
* returns an opaque signup token used by the resend and verify steps. This
|
|
725
|
+
* endpoint does not require an API key.
|
|
726
|
+
*
|
|
727
|
+
*/
|
|
728
|
+
const startCliSignup = (options) => (options.client ?? client).post({
|
|
729
|
+
url: "/cli/signup/start",
|
|
730
|
+
...options,
|
|
731
|
+
headers: {
|
|
732
|
+
...options.body !== void 0 && { "Content-Type": "application/json" },
|
|
733
|
+
...options.headers
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
/**
|
|
737
|
+
* Resend CLI signup verification code
|
|
738
|
+
*
|
|
739
|
+
* Sends a new email verification code for a pending CLI signup session.
|
|
740
|
+
* This endpoint does not require an API key.
|
|
741
|
+
*
|
|
742
|
+
*/
|
|
743
|
+
const resendCliSignupVerification = (options) => (options.client ?? client).post({
|
|
744
|
+
url: "/cli/signup/resend",
|
|
745
|
+
...options,
|
|
746
|
+
headers: {
|
|
747
|
+
...options.body !== void 0 && { "Content-Type": "application/json" },
|
|
748
|
+
...options.headers
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
/**
|
|
752
|
+
* Verify CLI signup and create API key
|
|
753
|
+
*
|
|
754
|
+
* Verifies the email code for a CLI signup session, creates the account,
|
|
755
|
+
* redeems the reserved signup code, mints an org-scoped CLI API key, and
|
|
756
|
+
* returns the raw key exactly once. This endpoint does not require an API key.
|
|
757
|
+
*
|
|
758
|
+
*/
|
|
759
|
+
const verifyCliSignup = (options) => (options.client ?? client).post({
|
|
760
|
+
url: "/cli/signup/verify",
|
|
761
|
+
...options,
|
|
762
|
+
headers: {
|
|
763
|
+
...options.body !== void 0 && { "Content-Type": "application/json" },
|
|
764
|
+
...options.headers
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
/**
|
|
714
768
|
* Revoke the current CLI API key
|
|
715
769
|
*
|
|
716
770
|
* Revokes the API key used to authenticate the request. CLI clients use
|
|
@@ -1451,8 +1505,9 @@ const listFunctions = (options) => (options?.client ?? client).get({
|
|
|
1451
1505
|
* than relying on external imports.
|
|
1452
1506
|
*
|
|
1453
1507
|
* **Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`
|
|
1454
|
-
* (optional) is capped at 5 MiB UTF-8
|
|
1455
|
-
*
|
|
1508
|
+
* (optional) is capped at 5 MiB UTF-8, stored with each deployment
|
|
1509
|
+
* attempt, and sent to the runtime so stack traces can resolve to
|
|
1510
|
+
* original source files.
|
|
1456
1511
|
*
|
|
1457
1512
|
* **Auto-wiring.** On successful deploy, Primitive automatically
|
|
1458
1513
|
* creates a webhook endpoint that delivers inbound mail to the
|
|
@@ -1525,11 +1580,10 @@ const getFunction = (options) => (options.client ?? client).get({
|
|
|
1525
1580
|
* passing the same `code` re-runs the deploy and refreshes the
|
|
1526
1581
|
* binding set with the latest values from the secrets table.
|
|
1527
1582
|
*
|
|
1528
|
-
* On
|
|
1529
|
-
*
|
|
1530
|
-
* `
|
|
1531
|
-
*
|
|
1532
|
-
* without polling.
|
|
1583
|
+
* On deploy failure, the previously-deployed code stays live; the
|
|
1584
|
+
* runtime never serves a half-built bundle. The response uses
|
|
1585
|
+
* `error.code` `deploy_failed`, and the function's `deploy_error`
|
|
1586
|
+
* field carries the latest deploy error for dashboard/API reads.
|
|
1533
1587
|
*
|
|
1534
1588
|
*/
|
|
1535
1589
|
const updateFunction = (options) => (options.client ?? client).put({
|
|
@@ -1582,6 +1636,24 @@ const testFunction = (options) => (options.client ?? client).post({
|
|
|
1582
1636
|
}
|
|
1583
1637
|
});
|
|
1584
1638
|
/**
|
|
1639
|
+
* Get a function test run trace
|
|
1640
|
+
*
|
|
1641
|
+
* Returns the current end-to-end trace for a function test run.
|
|
1642
|
+
* The trace is intentionally partial while the test is still in
|
|
1643
|
+
* flight: callers can poll this endpoint and watch it fill in
|
|
1644
|
+
* from send -> inbound -> webhook deliveries -> outbound
|
|
1645
|
+
* requests, logs, and replies.
|
|
1646
|
+
*
|
|
1647
|
+
*/
|
|
1648
|
+
const getFunctionTestRunTrace = (options) => (options.client ?? client).get({
|
|
1649
|
+
security: [{
|
|
1650
|
+
scheme: "bearer",
|
|
1651
|
+
type: "http"
|
|
1652
|
+
}],
|
|
1653
|
+
url: "/functions/{id}/test-runs/{run_id}/trace",
|
|
1654
|
+
...options
|
|
1655
|
+
});
|
|
1656
|
+
/**
|
|
1585
1657
|
* List a function's secrets
|
|
1586
1658
|
*
|
|
1587
1659
|
* Returns metadata for every secret bound to the function, with
|
|
@@ -1705,7 +1777,7 @@ const openapiDocument = {
|
|
|
1705
1777
|
"info": {
|
|
1706
1778
|
"title": "Primitive API",
|
|
1707
1779
|
"version": "1.0.0",
|
|
1708
|
-
"description": "The Primitive API lets you manage domains, emails, webhook endpoints,\nfilters, and account settings programmatically.\n\n## Authentication\n\
|
|
1780
|
+
"description": "The Primitive API lets you manage domains, emails, webhook endpoints,\nfilters, and account settings programmatically.\n\n## Authentication\n\nMost endpoints require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer prim_<your_api_key>\n```\n\nAPI keys are org-scoped. Create and manage them in your dashboard\nunder Settings > API Keys. CLI login and signup endpoints explicitly\ndeclare `security: []`; they do not require an API key because they\nare used to mint one.\n\n## Rate Limiting\n\nThe API enforces a sliding window rate limit of **120 requests per\n60 seconds** per organization. When exceeded, the API returns `429`\nwith a `Retry-After` header indicating how many seconds to wait.\n\n## Pagination\n\nList endpoints use cursor-based pagination. Responses include a\n`meta` object with `total`, `limit`, and `cursor` fields. Pass the\n`cursor` value as a query parameter to fetch the next page. When\n`cursor` is `null`, there are no more results.\n\n## Response Format\n\nAll responses use a consistent envelope:\n\n```json\n{\n \"success\": true,\n \"data\": { ... },\n \"meta\": { \"total\": 42, \"limit\": 50, \"cursor\": \"...\" }\n}\n```\n\nErrors follow the same pattern:\n\n```json\n{\n \"success\": false,\n \"error\": { \"code\": \"not_found\", \"message\": \"Email not found\" }\n}\n```\n\n## Webhook signing\n\nOutbound webhook deliveries (configured via the `endpoints` API)\nare signed so receivers can verify they came from Primitive and\nhave not been tampered with in transit. The signing scheme is\ndeliberately simple so it can be reimplemented in any language\nin a few lines. The Node SDK's `verifyWebhookSignature` helper\nis the reference implementation; the wire details below let you\nwrite a verifier in Python, Go, Ruby, etc. without reading our\nsource.\n\n**Header**: `Primitive-Signature: t=<unix-seconds>,v1=<hex>`\n\nA legacy `MyMX-Signature` header is also sent on every delivery\nwith the same value, retained for back-compatibility with\nintegrations written before the rename. New code should read\n`Primitive-Signature`.\n\n**Signed string**: `${timestamp}.${rawBody}` where `timestamp`\nis the Unix-seconds integer from the `t=` parameter and\n`rawBody` is the exact bytes of the HTTP request body BEFORE\nany JSON decoding. Verify against the raw body, not a\nre-serialized parse, or you will silently mismatch on\ninsignificant whitespace.\n\n**Signature**: HMAC-SHA256 of the signed string, hex-encoded\n(lowercase). Use the account's webhook secret as the HMAC key,\nas a UTF-8 byte sequence.\n\n**Secret**: returned by `GET /account/webhook-secret`. The\nstring looks base64-shaped (e.g. `XNHBBW8VqoBjRfNs1tkZj11jTk...`)\nbut is NOT base64; use it AS-IS as a UTF-8 string for the HMAC\nkey. Base64-decoding before HMAC will silently produce\nmismatched signatures.\n\n**Tolerance**: by convention, reject deliveries whose `t=`\ntimestamp is more than 5 minutes off your wall-clock to defend\nagainst replay attacks. The Node SDK's helper enforces this by\ndefault.\n\n**Verification recipe** (any language):\n\n```\n1. Read the raw HTTP body (do not parse).\n2. Read `Primitive-Signature: t=<ts>,v1=<sig>`.\n3. Reject if abs(now - ts) > 300 seconds.\n4. expected = HMAC_SHA256_hex(secret_utf8, f\"{ts}.{rawBody}\")\n5. Constant-time compare expected to sig. Reject if not equal.\n```\n\nFor Node, use `verifyWebhookSignature` from\n`@primitivedotdev/sdk/webhook` (or the higher-level\n`handleWebhook` helper if you want a one-liner). For other\nlanguages, the recipe above is everything you need.\n\nTest deliveries: `POST /endpoints/{id}/test` triggers a fake\ndelivery to your endpoint URL, signed with your real account\nsecret, so you can confirm verification end-to-end without\nneeding real inbound mail. The test response carries the exact\n`signature` header value sent on the wire so you can compare\nstrings directly.\n",
|
|
1709
1781
|
"contact": {
|
|
1710
1782
|
"name": "Primitive",
|
|
1711
1783
|
"url": "https://primitive.dev"
|
|
@@ -1877,6 +1949,97 @@ const openapiDocument = {
|
|
|
1877
1949
|
}
|
|
1878
1950
|
}
|
|
1879
1951
|
} },
|
|
1952
|
+
"/cli/signup/start": { "post": {
|
|
1953
|
+
"operationId": "startCliSignup",
|
|
1954
|
+
"summary": "Start CLI account signup",
|
|
1955
|
+
"description": "Starts a terminal-native CLI signup. The API validates the signup code,\ncreates a pending signup session, sends an email verification code, and\nreturns an opaque signup token used by the resend and verify steps. This\nendpoint does not require an API key.\n",
|
|
1956
|
+
"tags": ["CLI"],
|
|
1957
|
+
"security": [],
|
|
1958
|
+
"requestBody": {
|
|
1959
|
+
"required": true,
|
|
1960
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/StartCliSignupInput" } } }
|
|
1961
|
+
},
|
|
1962
|
+
"responses": {
|
|
1963
|
+
"201": {
|
|
1964
|
+
"description": "CLI signup session created and verification email sent",
|
|
1965
|
+
"headers": { "Cache-Control": {
|
|
1966
|
+
"schema": { "type": "string" },
|
|
1967
|
+
"description": "Always `no-store`"
|
|
1968
|
+
} },
|
|
1969
|
+
"content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
|
|
1970
|
+
"type": "object",
|
|
1971
|
+
"properties": { "data": { "$ref": "#/components/schemas/CliSignupStartResult" } }
|
|
1972
|
+
}] } } }
|
|
1973
|
+
},
|
|
1974
|
+
"400": { "$ref": "#/components/responses/ValidationError" },
|
|
1975
|
+
"429": { "$ref": "#/components/responses/RateLimited" }
|
|
1976
|
+
}
|
|
1977
|
+
} },
|
|
1978
|
+
"/cli/signup/resend": { "post": {
|
|
1979
|
+
"operationId": "resendCliSignupVerification",
|
|
1980
|
+
"summary": "Resend CLI signup verification code",
|
|
1981
|
+
"description": "Sends a new email verification code for a pending CLI signup session.\nThis endpoint does not require an API key.\n",
|
|
1982
|
+
"tags": ["CLI"],
|
|
1983
|
+
"security": [],
|
|
1984
|
+
"requestBody": {
|
|
1985
|
+
"required": true,
|
|
1986
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResendCliSignupVerificationInput" } } }
|
|
1987
|
+
},
|
|
1988
|
+
"responses": {
|
|
1989
|
+
"200": {
|
|
1990
|
+
"description": "Verification email resent",
|
|
1991
|
+
"headers": { "Cache-Control": {
|
|
1992
|
+
"schema": { "type": "string" },
|
|
1993
|
+
"description": "Always `no-store`"
|
|
1994
|
+
} },
|
|
1995
|
+
"content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
|
|
1996
|
+
"type": "object",
|
|
1997
|
+
"properties": { "data": { "$ref": "#/components/schemas/CliSignupResendResult" } }
|
|
1998
|
+
}] } } }
|
|
1999
|
+
},
|
|
2000
|
+
"400": {
|
|
2001
|
+
"description": "Invalid token or expired token",
|
|
2002
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
|
|
2003
|
+
},
|
|
2004
|
+
"429": {
|
|
2005
|
+
"description": "Global rate limit exceeded or resend requested too quickly",
|
|
2006
|
+
"headers": { "Retry-After": {
|
|
2007
|
+
"schema": { "type": "integer" },
|
|
2008
|
+
"description": "Seconds to wait before retrying"
|
|
2009
|
+
} },
|
|
2010
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
} },
|
|
2014
|
+
"/cli/signup/verify": { "post": {
|
|
2015
|
+
"operationId": "verifyCliSignup",
|
|
2016
|
+
"summary": "Verify CLI signup and create API key",
|
|
2017
|
+
"description": "Verifies the email code for a CLI signup session, creates the account,\nredeems the reserved signup code, mints an org-scoped CLI API key, and\nreturns the raw key exactly once. This endpoint does not require an API key.\n",
|
|
2018
|
+
"tags": ["CLI"],
|
|
2019
|
+
"security": [],
|
|
2020
|
+
"requestBody": {
|
|
2021
|
+
"required": true,
|
|
2022
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifyCliSignupInput" } } }
|
|
2023
|
+
},
|
|
2024
|
+
"responses": {
|
|
2025
|
+
"200": {
|
|
2026
|
+
"description": "CLI signup verified and API key created",
|
|
2027
|
+
"headers": { "Cache-Control": {
|
|
2028
|
+
"schema": { "type": "string" },
|
|
2029
|
+
"description": "Always `no-store`"
|
|
2030
|
+
} },
|
|
2031
|
+
"content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
|
|
2032
|
+
"type": "object",
|
|
2033
|
+
"properties": { "data": { "$ref": "#/components/schemas/CliSignupVerifyResult" } }
|
|
2034
|
+
}] } } }
|
|
2035
|
+
},
|
|
2036
|
+
"400": {
|
|
2037
|
+
"description": "Invalid request, invalid verification code, expired token, invalid signup code, rejected password, or account creation failure",
|
|
2038
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
|
|
2039
|
+
},
|
|
2040
|
+
"429": { "$ref": "#/components/responses/RateLimited" }
|
|
2041
|
+
}
|
|
2042
|
+
} },
|
|
1880
2043
|
"/cli/logout": { "post": {
|
|
1881
2044
|
"operationId": "cliLogout",
|
|
1882
2045
|
"summary": "Revoke the current CLI API key",
|
|
@@ -3017,7 +3180,7 @@ const openapiDocument = {
|
|
|
3017
3180
|
"post": {
|
|
3018
3181
|
"operationId": "createFunction",
|
|
3019
3182
|
"summary": "Deploy a function",
|
|
3020
|
-
"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). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan 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
|
|
3183
|
+
"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). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan 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 gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
|
|
3021
3184
|
"tags": ["Functions"],
|
|
3022
3185
|
"requestBody": {
|
|
3023
3186
|
"required": true,
|
|
@@ -3031,13 +3194,18 @@ const openapiDocument = {
|
|
|
3031
3194
|
"properties": { "data": { "$ref": "#/components/schemas/CreateFunctionResult" } }
|
|
3032
3195
|
}] } } }
|
|
3033
3196
|
},
|
|
3034
|
-
"400": {
|
|
3197
|
+
"400": {
|
|
3198
|
+
"description": "Invalid request parameters or customer-correctable deploy rejection",
|
|
3199
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
|
|
3200
|
+
},
|
|
3035
3201
|
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
3036
3202
|
"409": {
|
|
3037
3203
|
"description": "A function with this name already exists in the org",
|
|
3038
3204
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
|
|
3039
3205
|
},
|
|
3040
|
-
"
|
|
3206
|
+
"424": { "$ref": "#/components/responses/DeployFailed" },
|
|
3207
|
+
"429": { "$ref": "#/components/responses/DeployFailed" },
|
|
3208
|
+
"503": { "$ref": "#/components/responses/DeployFailed" }
|
|
3041
3209
|
}
|
|
3042
3210
|
}
|
|
3043
3211
|
},
|
|
@@ -3063,7 +3231,7 @@ const openapiDocument = {
|
|
|
3063
3231
|
"put": {
|
|
3064
3232
|
"operationId": "updateFunction",
|
|
3065
3233
|
"summary": "Update and redeploy a function",
|
|
3066
|
-
"description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn
|
|
3234
|
+
"description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn deploy failure, the previously-deployed code stays live; the\nruntime never serves a half-built bundle. The response uses\n`error.code` `deploy_failed`, and the function's `deploy_error`\nfield carries the latest deploy error for dashboard/API reads.\n",
|
|
3067
3235
|
"tags": ["Functions"],
|
|
3068
3236
|
"requestBody": {
|
|
3069
3237
|
"required": true,
|
|
@@ -3077,10 +3245,15 @@ const openapiDocument = {
|
|
|
3077
3245
|
"properties": { "data": { "$ref": "#/components/schemas/FunctionDetail" } }
|
|
3078
3246
|
}] } } }
|
|
3079
3247
|
},
|
|
3080
|
-
"400": {
|
|
3248
|
+
"400": {
|
|
3249
|
+
"description": "Invalid request parameters or customer-correctable deploy rejection",
|
|
3250
|
+
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
|
|
3251
|
+
},
|
|
3081
3252
|
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
3082
3253
|
"404": { "$ref": "#/components/responses/NotFound" },
|
|
3083
|
-
"
|
|
3254
|
+
"424": { "$ref": "#/components/responses/DeployFailed" },
|
|
3255
|
+
"429": { "$ref": "#/components/responses/DeployFailed" },
|
|
3256
|
+
"503": { "$ref": "#/components/responses/DeployFailed" }
|
|
3084
3257
|
}
|
|
3085
3258
|
},
|
|
3086
3259
|
"delete": {
|
|
@@ -3140,6 +3313,37 @@ const openapiDocument = {
|
|
|
3140
3313
|
}
|
|
3141
3314
|
}
|
|
3142
3315
|
},
|
|
3316
|
+
"/functions/{id}/test-runs/{run_id}/trace": {
|
|
3317
|
+
"parameters": [{ "$ref": "#/components/parameters/ResourceId" }, {
|
|
3318
|
+
"name": "run_id",
|
|
3319
|
+
"in": "path",
|
|
3320
|
+
"required": true,
|
|
3321
|
+
"description": "Function test run id returned by POST /functions/{id}/test.",
|
|
3322
|
+
"schema": {
|
|
3323
|
+
"type": "string",
|
|
3324
|
+
"format": "uuid"
|
|
3325
|
+
}
|
|
3326
|
+
}],
|
|
3327
|
+
"get": {
|
|
3328
|
+
"operationId": "getFunctionTestRunTrace",
|
|
3329
|
+
"summary": "Get a function test run trace",
|
|
3330
|
+
"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",
|
|
3331
|
+
"tags": ["Functions"],
|
|
3332
|
+
"responses": {
|
|
3333
|
+
"200": {
|
|
3334
|
+
"description": "Function test run trace",
|
|
3335
|
+
"content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
|
|
3336
|
+
"type": "object",
|
|
3337
|
+
"properties": { "data": { "$ref": "#/components/schemas/FunctionTestRunTrace" } }
|
|
3338
|
+
}] } } }
|
|
3339
|
+
},
|
|
3340
|
+
"400": { "$ref": "#/components/responses/ValidationError" },
|
|
3341
|
+
"401": { "$ref": "#/components/responses/Unauthorized" },
|
|
3342
|
+
"403": { "$ref": "#/components/responses/Forbidden" },
|
|
3343
|
+
"404": { "$ref": "#/components/responses/NotFound" }
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
},
|
|
3143
3347
|
"/functions/{id}/secrets": {
|
|
3144
3348
|
"parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
|
|
3145
3349
|
"get": {
|
|
@@ -3399,6 +3603,19 @@ const openapiDocument = {
|
|
|
3399
3603
|
}
|
|
3400
3604
|
} }
|
|
3401
3605
|
},
|
|
3606
|
+
"DeployFailed": {
|
|
3607
|
+
"description": "Function deploy could not be completed; previously deployed code remains live",
|
|
3608
|
+
"content": { "application/json": {
|
|
3609
|
+
"schema": { "$ref": "#/components/schemas/ErrorResponse" },
|
|
3610
|
+
"example": {
|
|
3611
|
+
"success": false,
|
|
3612
|
+
"error": {
|
|
3613
|
+
"code": "deploy_failed",
|
|
3614
|
+
"message": "Function deploy failed"
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
} }
|
|
3618
|
+
},
|
|
3402
3619
|
"RateLimited": {
|
|
3403
3620
|
"description": "Rate limit exceeded",
|
|
3404
3621
|
"headers": { "Retry-After": {
|
|
@@ -3758,6 +3975,162 @@ const openapiDocument = {
|
|
|
3758
3975
|
"org_name"
|
|
3759
3976
|
]
|
|
3760
3977
|
},
|
|
3978
|
+
"StartCliSignupInput": {
|
|
3979
|
+
"type": "object",
|
|
3980
|
+
"additionalProperties": false,
|
|
3981
|
+
"properties": {
|
|
3982
|
+
"email": {
|
|
3983
|
+
"type": "string",
|
|
3984
|
+
"format": "email",
|
|
3985
|
+
"maxLength": 254
|
|
3986
|
+
},
|
|
3987
|
+
"signup_code": {
|
|
3988
|
+
"type": "string",
|
|
3989
|
+
"minLength": 1,
|
|
3990
|
+
"maxLength": 128
|
|
3991
|
+
},
|
|
3992
|
+
"terms_accepted": {
|
|
3993
|
+
"type": "boolean",
|
|
3994
|
+
"const": true,
|
|
3995
|
+
"description": "Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy"
|
|
3996
|
+
},
|
|
3997
|
+
"device_name": {
|
|
3998
|
+
"type": "string",
|
|
3999
|
+
"minLength": 1,
|
|
4000
|
+
"maxLength": 80,
|
|
4001
|
+
"description": "Human-readable device name used for the created CLI API key"
|
|
4002
|
+
},
|
|
4003
|
+
"metadata": {
|
|
4004
|
+
"type": "object",
|
|
4005
|
+
"additionalProperties": true,
|
|
4006
|
+
"description": "Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer"
|
|
4007
|
+
}
|
|
4008
|
+
},
|
|
4009
|
+
"required": [
|
|
4010
|
+
"email",
|
|
4011
|
+
"signup_code",
|
|
4012
|
+
"terms_accepted"
|
|
4013
|
+
]
|
|
4014
|
+
},
|
|
4015
|
+
"CliSignupStartResult": {
|
|
4016
|
+
"type": "object",
|
|
4017
|
+
"properties": {
|
|
4018
|
+
"signup_token": {
|
|
4019
|
+
"type": "string",
|
|
4020
|
+
"description": "Opaque token used to verify or resend the pending CLI signup"
|
|
4021
|
+
},
|
|
4022
|
+
"email": {
|
|
4023
|
+
"type": "string",
|
|
4024
|
+
"format": "email"
|
|
4025
|
+
},
|
|
4026
|
+
"expires_in": {
|
|
4027
|
+
"type": "integer",
|
|
4028
|
+
"description": "Seconds until the pending signup expires"
|
|
4029
|
+
},
|
|
4030
|
+
"resend_after": {
|
|
4031
|
+
"type": "integer",
|
|
4032
|
+
"description": "Minimum seconds before requesting another verification email"
|
|
4033
|
+
},
|
|
4034
|
+
"verification_code_length": {
|
|
4035
|
+
"type": "integer",
|
|
4036
|
+
"description": "Number of digits in the emailed verification code"
|
|
4037
|
+
}
|
|
4038
|
+
},
|
|
4039
|
+
"required": [
|
|
4040
|
+
"signup_token",
|
|
4041
|
+
"email",
|
|
4042
|
+
"expires_in",
|
|
4043
|
+
"resend_after",
|
|
4044
|
+
"verification_code_length"
|
|
4045
|
+
]
|
|
4046
|
+
},
|
|
4047
|
+
"ResendCliSignupVerificationInput": {
|
|
4048
|
+
"type": "object",
|
|
4049
|
+
"additionalProperties": false,
|
|
4050
|
+
"properties": { "signup_token": {
|
|
4051
|
+
"type": "string",
|
|
4052
|
+
"minLength": 1
|
|
4053
|
+
} },
|
|
4054
|
+
"required": ["signup_token"]
|
|
4055
|
+
},
|
|
4056
|
+
"CliSignupResendResult": {
|
|
4057
|
+
"type": "object",
|
|
4058
|
+
"properties": {
|
|
4059
|
+
"email": {
|
|
4060
|
+
"type": "string",
|
|
4061
|
+
"format": "email"
|
|
4062
|
+
},
|
|
4063
|
+
"expires_in": {
|
|
4064
|
+
"type": "integer",
|
|
4065
|
+
"description": "Seconds until the pending signup expires"
|
|
4066
|
+
},
|
|
4067
|
+
"resend_after": {
|
|
4068
|
+
"type": "integer",
|
|
4069
|
+
"description": "Minimum seconds before requesting another verification email"
|
|
4070
|
+
},
|
|
4071
|
+
"verification_code_length": {
|
|
4072
|
+
"type": "integer",
|
|
4073
|
+
"description": "Number of digits in the emailed verification code"
|
|
4074
|
+
}
|
|
4075
|
+
},
|
|
4076
|
+
"required": [
|
|
4077
|
+
"email",
|
|
4078
|
+
"expires_in",
|
|
4079
|
+
"resend_after",
|
|
4080
|
+
"verification_code_length"
|
|
4081
|
+
]
|
|
4082
|
+
},
|
|
4083
|
+
"VerifyCliSignupInput": {
|
|
4084
|
+
"type": "object",
|
|
4085
|
+
"additionalProperties": false,
|
|
4086
|
+
"properties": {
|
|
4087
|
+
"signup_token": {
|
|
4088
|
+
"type": "string",
|
|
4089
|
+
"minLength": 1
|
|
4090
|
+
},
|
|
4091
|
+
"verification_code": {
|
|
4092
|
+
"type": "string",
|
|
4093
|
+
"minLength": 1,
|
|
4094
|
+
"maxLength": 32
|
|
4095
|
+
},
|
|
4096
|
+
"password": {
|
|
4097
|
+
"type": "string",
|
|
4098
|
+
"minLength": 1,
|
|
4099
|
+
"maxLength": 1024
|
|
4100
|
+
}
|
|
4101
|
+
},
|
|
4102
|
+
"required": [
|
|
4103
|
+
"signup_token",
|
|
4104
|
+
"verification_code",
|
|
4105
|
+
"password"
|
|
4106
|
+
]
|
|
4107
|
+
},
|
|
4108
|
+
"CliSignupVerifyResult": {
|
|
4109
|
+
"type": "object",
|
|
4110
|
+
"properties": {
|
|
4111
|
+
"api_key": {
|
|
4112
|
+
"type": "string",
|
|
4113
|
+
"description": "Newly-created API key for CLI authentication"
|
|
4114
|
+
},
|
|
4115
|
+
"key_id": {
|
|
4116
|
+
"type": "string",
|
|
4117
|
+
"format": "uuid"
|
|
4118
|
+
},
|
|
4119
|
+
"key_prefix": { "type": "string" },
|
|
4120
|
+
"org_id": {
|
|
4121
|
+
"type": "string",
|
|
4122
|
+
"format": "uuid"
|
|
4123
|
+
},
|
|
4124
|
+
"org_name": { "type": ["string", "null"] }
|
|
4125
|
+
},
|
|
4126
|
+
"required": [
|
|
4127
|
+
"api_key",
|
|
4128
|
+
"key_id",
|
|
4129
|
+
"key_prefix",
|
|
4130
|
+
"org_id",
|
|
4131
|
+
"org_name"
|
|
4132
|
+
]
|
|
4133
|
+
},
|
|
3761
4134
|
"CliLogoutInput": {
|
|
3762
4135
|
"type": "object",
|
|
3763
4136
|
"additionalProperties": false,
|
|
@@ -5224,7 +5597,7 @@ const openapiDocument = {
|
|
|
5224
5597
|
"type": "string",
|
|
5225
5598
|
"minLength": 1,
|
|
5226
5599
|
"maxLength": 5242880,
|
|
5227
|
-
"description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored
|
|
5600
|
+
"description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
|
|
5228
5601
|
}
|
|
5229
5602
|
},
|
|
5230
5603
|
"required": ["name", "code"]
|
|
@@ -5271,11 +5644,16 @@ const openapiDocument = {
|
|
|
5271
5644
|
},
|
|
5272
5645
|
"TestInvocationResult": {
|
|
5273
5646
|
"type": "object",
|
|
5274
|
-
"description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued;
|
|
5647
|
+
"description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; poll `trace_url` to watch the run progress through\nsend -> inbound -> webhook deliveries -> outbound requests,\nlogs, and replies.\n",
|
|
5275
5648
|
"properties": {
|
|
5276
|
-
"
|
|
5649
|
+
"test_run_id": {
|
|
5277
5650
|
"type": "string",
|
|
5278
|
-
"
|
|
5651
|
+
"format": "uuid",
|
|
5652
|
+
"description": "Durable test run id used to fetch the run trace."
|
|
5653
|
+
},
|
|
5654
|
+
"inbound_domain": {
|
|
5655
|
+
"type": "string",
|
|
5656
|
+
"description": "Verified inbound domain the test email was sent to."
|
|
5279
5657
|
},
|
|
5280
5658
|
"to": {
|
|
5281
5659
|
"type": "string",
|
|
@@ -5302,16 +5680,339 @@ const openapiDocument = {
|
|
|
5302
5680
|
"type": "string",
|
|
5303
5681
|
"format": "uri",
|
|
5304
5682
|
"description": "Function detail page where invocations show up live."
|
|
5683
|
+
},
|
|
5684
|
+
"trace_url": {
|
|
5685
|
+
"type": "string",
|
|
5686
|
+
"description": "Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace."
|
|
5305
5687
|
}
|
|
5306
5688
|
},
|
|
5307
5689
|
"required": [
|
|
5690
|
+
"test_run_id",
|
|
5308
5691
|
"inbound_domain",
|
|
5309
5692
|
"to",
|
|
5310
5693
|
"from",
|
|
5311
5694
|
"send_id",
|
|
5312
5695
|
"subject",
|
|
5313
5696
|
"poll_since",
|
|
5314
|
-
"watch_url"
|
|
5697
|
+
"watch_url",
|
|
5698
|
+
"trace_url"
|
|
5699
|
+
]
|
|
5700
|
+
},
|
|
5701
|
+
"FunctionTestRunState": {
|
|
5702
|
+
"type": "string",
|
|
5703
|
+
"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",
|
|
5704
|
+
"enum": [
|
|
5705
|
+
"send_failed",
|
|
5706
|
+
"waiting_for_send",
|
|
5707
|
+
"waiting_for_inbound",
|
|
5708
|
+
"waiting_for_function",
|
|
5709
|
+
"completed",
|
|
5710
|
+
"failed"
|
|
5711
|
+
]
|
|
5712
|
+
},
|
|
5713
|
+
"FunctionTestRun": {
|
|
5714
|
+
"type": "object",
|
|
5715
|
+
"properties": {
|
|
5716
|
+
"id": {
|
|
5717
|
+
"type": "string",
|
|
5718
|
+
"format": "uuid"
|
|
5719
|
+
},
|
|
5720
|
+
"function_id": {
|
|
5721
|
+
"type": "string",
|
|
5722
|
+
"format": "uuid"
|
|
5723
|
+
},
|
|
5724
|
+
"inbound_domain": { "type": "string" },
|
|
5725
|
+
"to": { "type": "string" },
|
|
5726
|
+
"from": { "type": "string" },
|
|
5727
|
+
"subject": { "type": "string" },
|
|
5728
|
+
"poll_since": {
|
|
5729
|
+
"type": "string",
|
|
5730
|
+
"format": "date-time"
|
|
5731
|
+
},
|
|
5732
|
+
"created_at": {
|
|
5733
|
+
"type": "string",
|
|
5734
|
+
"format": "date-time"
|
|
5735
|
+
},
|
|
5736
|
+
"sent_at": {
|
|
5737
|
+
"type": ["string", "null"],
|
|
5738
|
+
"format": "date-time"
|
|
5739
|
+
},
|
|
5740
|
+
"send_error": { "type": ["string", "null"] }
|
|
5741
|
+
},
|
|
5742
|
+
"required": [
|
|
5743
|
+
"id",
|
|
5744
|
+
"function_id",
|
|
5745
|
+
"inbound_domain",
|
|
5746
|
+
"to",
|
|
5747
|
+
"from",
|
|
5748
|
+
"subject",
|
|
5749
|
+
"poll_since",
|
|
5750
|
+
"created_at",
|
|
5751
|
+
"sent_at",
|
|
5752
|
+
"send_error"
|
|
5753
|
+
]
|
|
5754
|
+
},
|
|
5755
|
+
"FunctionTestRunSend": {
|
|
5756
|
+
"type": ["object", "null"],
|
|
5757
|
+
"properties": {
|
|
5758
|
+
"id": {
|
|
5759
|
+
"type": "string",
|
|
5760
|
+
"format": "uuid"
|
|
5761
|
+
},
|
|
5762
|
+
"status": { "$ref": "#/components/schemas/SentEmailStatus" },
|
|
5763
|
+
"queue_id": { "type": ["string", "null"] },
|
|
5764
|
+
"created_at": {
|
|
5765
|
+
"type": "string",
|
|
5766
|
+
"format": "date-time"
|
|
5767
|
+
},
|
|
5768
|
+
"updated_at": {
|
|
5769
|
+
"type": "string",
|
|
5770
|
+
"format": "date-time"
|
|
5771
|
+
}
|
|
5772
|
+
},
|
|
5773
|
+
"required": [
|
|
5774
|
+
"id",
|
|
5775
|
+
"status",
|
|
5776
|
+
"queue_id",
|
|
5777
|
+
"created_at",
|
|
5778
|
+
"updated_at"
|
|
5779
|
+
]
|
|
5780
|
+
},
|
|
5781
|
+
"FunctionTestRunInboundEmail": {
|
|
5782
|
+
"type": ["object", "null"],
|
|
5783
|
+
"properties": {
|
|
5784
|
+
"id": {
|
|
5785
|
+
"type": "string",
|
|
5786
|
+
"format": "uuid"
|
|
5787
|
+
},
|
|
5788
|
+
"status": { "$ref": "#/components/schemas/EmailStatus" },
|
|
5789
|
+
"received_at": {
|
|
5790
|
+
"type": "string",
|
|
5791
|
+
"format": "date-time"
|
|
5792
|
+
},
|
|
5793
|
+
"from": { "type": "string" },
|
|
5794
|
+
"to": { "type": "string" },
|
|
5795
|
+
"subject": { "type": ["string", "null"] },
|
|
5796
|
+
"webhook_status": { "$ref": "#/components/schemas/EmailWebhookStatus" },
|
|
5797
|
+
"webhook_attempt_count": { "type": "integer" },
|
|
5798
|
+
"webhook_last_status_code": { "type": ["integer", "null"] },
|
|
5799
|
+
"webhook_last_error": { "type": ["string", "null"] }
|
|
5800
|
+
},
|
|
5801
|
+
"required": [
|
|
5802
|
+
"id",
|
|
5803
|
+
"status",
|
|
5804
|
+
"received_at",
|
|
5805
|
+
"from",
|
|
5806
|
+
"to",
|
|
5807
|
+
"subject",
|
|
5808
|
+
"webhook_status",
|
|
5809
|
+
"webhook_attempt_count",
|
|
5810
|
+
"webhook_last_status_code",
|
|
5811
|
+
"webhook_last_error"
|
|
5812
|
+
]
|
|
5813
|
+
},
|
|
5814
|
+
"FunctionTestRunDeliveryEndpoint": {
|
|
5815
|
+
"type": ["object", "null"],
|
|
5816
|
+
"properties": {
|
|
5817
|
+
"id": {
|
|
5818
|
+
"type": "string",
|
|
5819
|
+
"format": "uuid"
|
|
5820
|
+
},
|
|
5821
|
+
"kind": {
|
|
5822
|
+
"type": "string",
|
|
5823
|
+
"description": "Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear."
|
|
5824
|
+
},
|
|
5825
|
+
"function_id": {
|
|
5826
|
+
"type": ["string", "null"],
|
|
5827
|
+
"format": "uuid"
|
|
5828
|
+
},
|
|
5829
|
+
"function_name": { "type": ["string", "null"] },
|
|
5830
|
+
"domain_id": {
|
|
5831
|
+
"type": ["string", "null"],
|
|
5832
|
+
"format": "uuid"
|
|
5833
|
+
},
|
|
5834
|
+
"enabled": { "type": "boolean" },
|
|
5835
|
+
"deactivated_at": {
|
|
5836
|
+
"type": ["string", "null"],
|
|
5837
|
+
"format": "date-time"
|
|
5838
|
+
},
|
|
5839
|
+
"is_current_function": { "type": "boolean" }
|
|
5840
|
+
},
|
|
5841
|
+
"required": [
|
|
5842
|
+
"id",
|
|
5843
|
+
"kind",
|
|
5844
|
+
"function_id",
|
|
5845
|
+
"function_name",
|
|
5846
|
+
"domain_id",
|
|
5847
|
+
"enabled",
|
|
5848
|
+
"deactivated_at",
|
|
5849
|
+
"is_current_function"
|
|
5850
|
+
]
|
|
5851
|
+
},
|
|
5852
|
+
"FunctionTestRunDelivery": {
|
|
5853
|
+
"type": "object",
|
|
5854
|
+
"properties": {
|
|
5855
|
+
"id": {
|
|
5856
|
+
"type": "string",
|
|
5857
|
+
"description": "Webhook delivery id."
|
|
5858
|
+
},
|
|
5859
|
+
"endpoint_id": {
|
|
5860
|
+
"type": "string",
|
|
5861
|
+
"format": "uuid"
|
|
5862
|
+
},
|
|
5863
|
+
"endpoint_url": {
|
|
5864
|
+
"type": "string",
|
|
5865
|
+
"format": "uri"
|
|
5866
|
+
},
|
|
5867
|
+
"status": {
|
|
5868
|
+
"type": "string",
|
|
5869
|
+
"enum": [
|
|
5870
|
+
"pending",
|
|
5871
|
+
"delivered",
|
|
5872
|
+
"header_confirmed",
|
|
5873
|
+
"failed"
|
|
5874
|
+
]
|
|
5875
|
+
},
|
|
5876
|
+
"attempt_count": { "type": "integer" },
|
|
5877
|
+
"duration_ms": { "type": ["integer", "null"] },
|
|
5878
|
+
"last_error": { "type": ["string", "null"] },
|
|
5879
|
+
"last_error_code": { "type": ["string", "null"] },
|
|
5880
|
+
"created_at": {
|
|
5881
|
+
"type": "string",
|
|
5882
|
+
"format": "date-time"
|
|
5883
|
+
},
|
|
5884
|
+
"updated_at": {
|
|
5885
|
+
"type": "string",
|
|
5886
|
+
"format": "date-time"
|
|
5887
|
+
},
|
|
5888
|
+
"endpoint": { "$ref": "#/components/schemas/FunctionTestRunDeliveryEndpoint" }
|
|
5889
|
+
},
|
|
5890
|
+
"required": [
|
|
5891
|
+
"id",
|
|
5892
|
+
"endpoint_id",
|
|
5893
|
+
"endpoint_url",
|
|
5894
|
+
"status",
|
|
5895
|
+
"attempt_count",
|
|
5896
|
+
"duration_ms",
|
|
5897
|
+
"last_error",
|
|
5898
|
+
"last_error_code",
|
|
5899
|
+
"created_at",
|
|
5900
|
+
"updated_at",
|
|
5901
|
+
"endpoint"
|
|
5902
|
+
]
|
|
5903
|
+
},
|
|
5904
|
+
"FunctionTestRunOutboundRequest": {
|
|
5905
|
+
"type": "object",
|
|
5906
|
+
"properties": {
|
|
5907
|
+
"id": {
|
|
5908
|
+
"type": "string",
|
|
5909
|
+
"format": "uuid"
|
|
5910
|
+
},
|
|
5911
|
+
"function_id": {
|
|
5912
|
+
"type": "string",
|
|
5913
|
+
"format": "uuid"
|
|
5914
|
+
},
|
|
5915
|
+
"webhook_delivery_id": { "type": ["string", "null"] },
|
|
5916
|
+
"email_id": {
|
|
5917
|
+
"type": ["string", "null"],
|
|
5918
|
+
"format": "uuid"
|
|
5919
|
+
},
|
|
5920
|
+
"endpoint_id": {
|
|
5921
|
+
"type": ["string", "null"],
|
|
5922
|
+
"format": "uuid"
|
|
5923
|
+
},
|
|
5924
|
+
"method": { "type": "string" },
|
|
5925
|
+
"url": {
|
|
5926
|
+
"type": "string",
|
|
5927
|
+
"format": "uri"
|
|
5928
|
+
},
|
|
5929
|
+
"host": { "type": "string" },
|
|
5930
|
+
"path": { "type": "string" },
|
|
5931
|
+
"status_code": { "type": ["integer", "null"] },
|
|
5932
|
+
"ok": { "type": ["boolean", "null"] },
|
|
5933
|
+
"duration_ms": { "type": "integer" },
|
|
5934
|
+
"error": { "type": ["string", "null"] },
|
|
5935
|
+
"ts": {
|
|
5936
|
+
"type": "string",
|
|
5937
|
+
"format": "date-time"
|
|
5938
|
+
}
|
|
5939
|
+
},
|
|
5940
|
+
"required": [
|
|
5941
|
+
"id",
|
|
5942
|
+
"function_id",
|
|
5943
|
+
"webhook_delivery_id",
|
|
5944
|
+
"email_id",
|
|
5945
|
+
"endpoint_id",
|
|
5946
|
+
"method",
|
|
5947
|
+
"url",
|
|
5948
|
+
"host",
|
|
5949
|
+
"path",
|
|
5950
|
+
"status_code",
|
|
5951
|
+
"ok",
|
|
5952
|
+
"duration_ms",
|
|
5953
|
+
"error",
|
|
5954
|
+
"ts"
|
|
5955
|
+
]
|
|
5956
|
+
},
|
|
5957
|
+
"FunctionTestRunReply": {
|
|
5958
|
+
"type": "object",
|
|
5959
|
+
"properties": {
|
|
5960
|
+
"id": {
|
|
5961
|
+
"type": "string",
|
|
5962
|
+
"format": "uuid"
|
|
5963
|
+
},
|
|
5964
|
+
"status": { "$ref": "#/components/schemas/SentEmailStatus" },
|
|
5965
|
+
"to": { "type": "string" },
|
|
5966
|
+
"subject": { "type": "string" },
|
|
5967
|
+
"queue_id": { "type": ["string", "null"] },
|
|
5968
|
+
"created_at": {
|
|
5969
|
+
"type": "string",
|
|
5970
|
+
"format": "date-time"
|
|
5971
|
+
}
|
|
5972
|
+
},
|
|
5973
|
+
"required": [
|
|
5974
|
+
"id",
|
|
5975
|
+
"status",
|
|
5976
|
+
"to",
|
|
5977
|
+
"subject",
|
|
5978
|
+
"queue_id",
|
|
5979
|
+
"created_at"
|
|
5980
|
+
]
|
|
5981
|
+
},
|
|
5982
|
+
"FunctionTestRunTrace": {
|
|
5983
|
+
"type": "object",
|
|
5984
|
+
"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",
|
|
5985
|
+
"properties": {
|
|
5986
|
+
"state": { "$ref": "#/components/schemas/FunctionTestRunState" },
|
|
5987
|
+
"test_run": { "$ref": "#/components/schemas/FunctionTestRun" },
|
|
5988
|
+
"test_send": { "$ref": "#/components/schemas/FunctionTestRunSend" },
|
|
5989
|
+
"inbound_email": { "$ref": "#/components/schemas/FunctionTestRunInboundEmail" },
|
|
5990
|
+
"deliveries": {
|
|
5991
|
+
"type": "array",
|
|
5992
|
+
"items": { "$ref": "#/components/schemas/FunctionTestRunDelivery" }
|
|
5993
|
+
},
|
|
5994
|
+
"outbound_requests": {
|
|
5995
|
+
"type": "array",
|
|
5996
|
+
"items": { "$ref": "#/components/schemas/FunctionTestRunOutboundRequest" }
|
|
5997
|
+
},
|
|
5998
|
+
"logs": {
|
|
5999
|
+
"type": "array",
|
|
6000
|
+
"items": { "$ref": "#/components/schemas/FunctionLogRow" }
|
|
6001
|
+
},
|
|
6002
|
+
"replies": {
|
|
6003
|
+
"type": "array",
|
|
6004
|
+
"items": { "$ref": "#/components/schemas/FunctionTestRunReply" }
|
|
6005
|
+
}
|
|
6006
|
+
},
|
|
6007
|
+
"required": [
|
|
6008
|
+
"state",
|
|
6009
|
+
"test_run",
|
|
6010
|
+
"test_send",
|
|
6011
|
+
"inbound_email",
|
|
6012
|
+
"deliveries",
|
|
6013
|
+
"outbound_requests",
|
|
6014
|
+
"logs",
|
|
6015
|
+
"replies"
|
|
5315
6016
|
]
|
|
5316
6017
|
},
|
|
5317
6018
|
"FunctionLogRow": {
|
|
@@ -5756,6 +6457,58 @@ const operationManifest = [
|
|
|
5756
6457
|
"tag": "CLI",
|
|
5757
6458
|
"tagCommand": "cli"
|
|
5758
6459
|
},
|
|
6460
|
+
{
|
|
6461
|
+
"binaryResponse": false,
|
|
6462
|
+
"bodyRequired": true,
|
|
6463
|
+
"command": "resend-cli-signup-verification",
|
|
6464
|
+
"description": "Sends a new email verification code for a pending CLI signup session.\nThis endpoint does not require an API key.\n",
|
|
6465
|
+
"hasJsonBody": true,
|
|
6466
|
+
"method": "POST",
|
|
6467
|
+
"operationId": "resendCliSignupVerification",
|
|
6468
|
+
"path": "/cli/signup/resend",
|
|
6469
|
+
"pathParams": [],
|
|
6470
|
+
"queryParams": [],
|
|
6471
|
+
"requestSchema": {
|
|
6472
|
+
"type": "object",
|
|
6473
|
+
"additionalProperties": false,
|
|
6474
|
+
"properties": { "signup_token": {
|
|
6475
|
+
"type": "string",
|
|
6476
|
+
"minLength": 1
|
|
6477
|
+
} },
|
|
6478
|
+
"required": ["signup_token"]
|
|
6479
|
+
},
|
|
6480
|
+
"responseSchema": {
|
|
6481
|
+
"type": "object",
|
|
6482
|
+
"properties": {
|
|
6483
|
+
"email": {
|
|
6484
|
+
"type": "string",
|
|
6485
|
+
"format": "email"
|
|
6486
|
+
},
|
|
6487
|
+
"expires_in": {
|
|
6488
|
+
"type": "integer",
|
|
6489
|
+
"description": "Seconds until the pending signup expires"
|
|
6490
|
+
},
|
|
6491
|
+
"resend_after": {
|
|
6492
|
+
"type": "integer",
|
|
6493
|
+
"description": "Minimum seconds before requesting another verification email"
|
|
6494
|
+
},
|
|
6495
|
+
"verification_code_length": {
|
|
6496
|
+
"type": "integer",
|
|
6497
|
+
"description": "Number of digits in the emailed verification code"
|
|
6498
|
+
}
|
|
6499
|
+
},
|
|
6500
|
+
"required": [
|
|
6501
|
+
"email",
|
|
6502
|
+
"expires_in",
|
|
6503
|
+
"resend_after",
|
|
6504
|
+
"verification_code_length"
|
|
6505
|
+
]
|
|
6506
|
+
},
|
|
6507
|
+
"sdkName": "resendCliSignupVerification",
|
|
6508
|
+
"summary": "Resend CLI signup verification code",
|
|
6509
|
+
"tag": "CLI",
|
|
6510
|
+
"tagCommand": "cli"
|
|
6511
|
+
},
|
|
5759
6512
|
{
|
|
5760
6513
|
"binaryResponse": false,
|
|
5761
6514
|
"bodyRequired": false,
|
|
@@ -5802,28 +6555,180 @@ const operationManifest = [
|
|
|
5802
6555
|
},
|
|
5803
6556
|
"verification_uri_complete": {
|
|
5804
6557
|
"type": "string",
|
|
5805
|
-
"description": "Browser URL with the user code prefilled"
|
|
5806
|
-
},
|
|
5807
|
-
"expires_in": {
|
|
5808
|
-
"type": "integer",
|
|
5809
|
-
"description": "Seconds until the login session expires"
|
|
6558
|
+
"description": "Browser URL with the user code prefilled"
|
|
6559
|
+
},
|
|
6560
|
+
"expires_in": {
|
|
6561
|
+
"type": "integer",
|
|
6562
|
+
"description": "Seconds until the login session expires"
|
|
6563
|
+
},
|
|
6564
|
+
"interval": {
|
|
6565
|
+
"type": "integer",
|
|
6566
|
+
"description": "Minimum seconds between poll requests"
|
|
6567
|
+
}
|
|
6568
|
+
},
|
|
6569
|
+
"required": [
|
|
6570
|
+
"device_code",
|
|
6571
|
+
"user_code",
|
|
6572
|
+
"verification_uri",
|
|
6573
|
+
"verification_uri_complete",
|
|
6574
|
+
"expires_in",
|
|
6575
|
+
"interval"
|
|
6576
|
+
]
|
|
6577
|
+
},
|
|
6578
|
+
"sdkName": "startCliLogin",
|
|
6579
|
+
"summary": "Start CLI browser login",
|
|
6580
|
+
"tag": "CLI",
|
|
6581
|
+
"tagCommand": "cli"
|
|
6582
|
+
},
|
|
6583
|
+
{
|
|
6584
|
+
"binaryResponse": false,
|
|
6585
|
+
"bodyRequired": true,
|
|
6586
|
+
"command": "start-cli-signup",
|
|
6587
|
+
"description": "Starts a terminal-native CLI signup. The API validates the signup code,\ncreates a pending signup session, sends an email verification code, and\nreturns an opaque signup token used by the resend and verify steps. This\nendpoint does not require an API key.\n",
|
|
6588
|
+
"hasJsonBody": true,
|
|
6589
|
+
"method": "POST",
|
|
6590
|
+
"operationId": "startCliSignup",
|
|
6591
|
+
"path": "/cli/signup/start",
|
|
6592
|
+
"pathParams": [],
|
|
6593
|
+
"queryParams": [],
|
|
6594
|
+
"requestSchema": {
|
|
6595
|
+
"type": "object",
|
|
6596
|
+
"additionalProperties": false,
|
|
6597
|
+
"properties": {
|
|
6598
|
+
"email": {
|
|
6599
|
+
"type": "string",
|
|
6600
|
+
"format": "email",
|
|
6601
|
+
"maxLength": 254
|
|
6602
|
+
},
|
|
6603
|
+
"signup_code": {
|
|
6604
|
+
"type": "string",
|
|
6605
|
+
"minLength": 1,
|
|
6606
|
+
"maxLength": 128
|
|
6607
|
+
},
|
|
6608
|
+
"terms_accepted": {
|
|
6609
|
+
"type": "boolean",
|
|
6610
|
+
"const": true,
|
|
6611
|
+
"description": "Must be true to confirm acceptance of Primitive's Terms of Service and Privacy Policy"
|
|
6612
|
+
},
|
|
6613
|
+
"device_name": {
|
|
6614
|
+
"type": "string",
|
|
6615
|
+
"minLength": 1,
|
|
6616
|
+
"maxLength": 80,
|
|
6617
|
+
"description": "Human-readable device name used for the created CLI API key"
|
|
6618
|
+
},
|
|
6619
|
+
"metadata": {
|
|
6620
|
+
"type": "object",
|
|
6621
|
+
"additionalProperties": true,
|
|
6622
|
+
"description": "Optional client metadata stored with the signup session; serialized JSON must be 2048 bytes or fewer"
|
|
6623
|
+
}
|
|
6624
|
+
},
|
|
6625
|
+
"required": [
|
|
6626
|
+
"email",
|
|
6627
|
+
"signup_code",
|
|
6628
|
+
"terms_accepted"
|
|
6629
|
+
]
|
|
6630
|
+
},
|
|
6631
|
+
"responseSchema": {
|
|
6632
|
+
"type": "object",
|
|
6633
|
+
"properties": {
|
|
6634
|
+
"signup_token": {
|
|
6635
|
+
"type": "string",
|
|
6636
|
+
"description": "Opaque token used to verify or resend the pending CLI signup"
|
|
6637
|
+
},
|
|
6638
|
+
"email": {
|
|
6639
|
+
"type": "string",
|
|
6640
|
+
"format": "email"
|
|
6641
|
+
},
|
|
6642
|
+
"expires_in": {
|
|
6643
|
+
"type": "integer",
|
|
6644
|
+
"description": "Seconds until the pending signup expires"
|
|
6645
|
+
},
|
|
6646
|
+
"resend_after": {
|
|
6647
|
+
"type": "integer",
|
|
6648
|
+
"description": "Minimum seconds before requesting another verification email"
|
|
6649
|
+
},
|
|
6650
|
+
"verification_code_length": {
|
|
6651
|
+
"type": "integer",
|
|
6652
|
+
"description": "Number of digits in the emailed verification code"
|
|
6653
|
+
}
|
|
6654
|
+
},
|
|
6655
|
+
"required": [
|
|
6656
|
+
"signup_token",
|
|
6657
|
+
"email",
|
|
6658
|
+
"expires_in",
|
|
6659
|
+
"resend_after",
|
|
6660
|
+
"verification_code_length"
|
|
6661
|
+
]
|
|
6662
|
+
},
|
|
6663
|
+
"sdkName": "startCliSignup",
|
|
6664
|
+
"summary": "Start CLI account signup",
|
|
6665
|
+
"tag": "CLI",
|
|
6666
|
+
"tagCommand": "cli"
|
|
6667
|
+
},
|
|
6668
|
+
{
|
|
6669
|
+
"binaryResponse": false,
|
|
6670
|
+
"bodyRequired": true,
|
|
6671
|
+
"command": "verify-cli-signup",
|
|
6672
|
+
"description": "Verifies the email code for a CLI signup session, creates the account,\nredeems the reserved signup code, mints an org-scoped CLI API key, and\nreturns the raw key exactly once. This endpoint does not require an API key.\n",
|
|
6673
|
+
"hasJsonBody": true,
|
|
6674
|
+
"method": "POST",
|
|
6675
|
+
"operationId": "verifyCliSignup",
|
|
6676
|
+
"path": "/cli/signup/verify",
|
|
6677
|
+
"pathParams": [],
|
|
6678
|
+
"queryParams": [],
|
|
6679
|
+
"requestSchema": {
|
|
6680
|
+
"type": "object",
|
|
6681
|
+
"additionalProperties": false,
|
|
6682
|
+
"properties": {
|
|
6683
|
+
"signup_token": {
|
|
6684
|
+
"type": "string",
|
|
6685
|
+
"minLength": 1
|
|
6686
|
+
},
|
|
6687
|
+
"verification_code": {
|
|
6688
|
+
"type": "string",
|
|
6689
|
+
"minLength": 1,
|
|
6690
|
+
"maxLength": 32
|
|
6691
|
+
},
|
|
6692
|
+
"password": {
|
|
6693
|
+
"type": "string",
|
|
6694
|
+
"minLength": 1,
|
|
6695
|
+
"maxLength": 1024
|
|
6696
|
+
}
|
|
6697
|
+
},
|
|
6698
|
+
"required": [
|
|
6699
|
+
"signup_token",
|
|
6700
|
+
"verification_code",
|
|
6701
|
+
"password"
|
|
6702
|
+
]
|
|
6703
|
+
},
|
|
6704
|
+
"responseSchema": {
|
|
6705
|
+
"type": "object",
|
|
6706
|
+
"properties": {
|
|
6707
|
+
"api_key": {
|
|
6708
|
+
"type": "string",
|
|
6709
|
+
"description": "Newly-created API key for CLI authentication"
|
|
6710
|
+
},
|
|
6711
|
+
"key_id": {
|
|
6712
|
+
"type": "string",
|
|
6713
|
+
"format": "uuid"
|
|
6714
|
+
},
|
|
6715
|
+
"key_prefix": { "type": "string" },
|
|
6716
|
+
"org_id": {
|
|
6717
|
+
"type": "string",
|
|
6718
|
+
"format": "uuid"
|
|
5810
6719
|
},
|
|
5811
|
-
"
|
|
5812
|
-
"type": "integer",
|
|
5813
|
-
"description": "Minimum seconds between poll requests"
|
|
5814
|
-
}
|
|
6720
|
+
"org_name": { "type": ["string", "null"] }
|
|
5815
6721
|
},
|
|
5816
6722
|
"required": [
|
|
5817
|
-
"
|
|
5818
|
-
"
|
|
5819
|
-
"
|
|
5820
|
-
"
|
|
5821
|
-
"
|
|
5822
|
-
"interval"
|
|
6723
|
+
"api_key",
|
|
6724
|
+
"key_id",
|
|
6725
|
+
"key_prefix",
|
|
6726
|
+
"org_id",
|
|
6727
|
+
"org_name"
|
|
5823
6728
|
]
|
|
5824
6729
|
},
|
|
5825
|
-
"sdkName": "
|
|
5826
|
-
"summary": "
|
|
6730
|
+
"sdkName": "verifyCliSignup",
|
|
6731
|
+
"summary": "Verify CLI signup and create API key",
|
|
5827
6732
|
"tag": "CLI",
|
|
5828
6733
|
"tagCommand": "cli"
|
|
5829
6734
|
},
|
|
@@ -7497,7 +8402,7 @@ const operationManifest = [
|
|
|
7497
8402
|
"binaryResponse": false,
|
|
7498
8403
|
"bodyRequired": true,
|
|
7499
8404
|
"command": "create-function",
|
|
7500
|
-
"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). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan 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
|
|
8405
|
+
"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). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan 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 gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
|
|
7501
8406
|
"hasJsonBody": true,
|
|
7502
8407
|
"method": "POST",
|
|
7503
8408
|
"operationId": "createFunction",
|
|
@@ -7523,7 +8428,7 @@ const operationManifest = [
|
|
|
7523
8428
|
"type": "string",
|
|
7524
8429
|
"minLength": 1,
|
|
7525
8430
|
"maxLength": 5242880,
|
|
7526
|
-
"description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored
|
|
8431
|
+
"description": "Optional source map for the bundle. Up to 5 MiB UTF-8.\nStored with the deployment attempt and sent to the runtime\nto symbolicate stack traces in the function's logs.\n"
|
|
7527
8432
|
}
|
|
7528
8433
|
},
|
|
7529
8434
|
"required": ["name", "code"]
|
|
@@ -7745,17 +8650,450 @@ const operationManifest = [
|
|
|
7745
8650
|
}
|
|
7746
8651
|
},
|
|
7747
8652
|
"required": [
|
|
7748
|
-
"id",
|
|
7749
|
-
"name",
|
|
7750
|
-
"code",
|
|
7751
|
-
"deploy_status",
|
|
7752
|
-
"gateway_url",
|
|
7753
|
-
"created_at",
|
|
7754
|
-
"updated_at"
|
|
8653
|
+
"id",
|
|
8654
|
+
"name",
|
|
8655
|
+
"code",
|
|
8656
|
+
"deploy_status",
|
|
8657
|
+
"gateway_url",
|
|
8658
|
+
"created_at",
|
|
8659
|
+
"updated_at"
|
|
8660
|
+
]
|
|
8661
|
+
},
|
|
8662
|
+
"sdkName": "getFunction",
|
|
8663
|
+
"summary": "Get a function",
|
|
8664
|
+
"tag": "Functions",
|
|
8665
|
+
"tagCommand": "functions"
|
|
8666
|
+
},
|
|
8667
|
+
{
|
|
8668
|
+
"binaryResponse": false,
|
|
8669
|
+
"bodyRequired": false,
|
|
8670
|
+
"command": "get-function-test-run-trace",
|
|
8671
|
+
"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",
|
|
8672
|
+
"hasJsonBody": false,
|
|
8673
|
+
"method": "GET",
|
|
8674
|
+
"operationId": "getFunctionTestRunTrace",
|
|
8675
|
+
"path": "/functions/{id}/test-runs/{run_id}/trace",
|
|
8676
|
+
"pathParams": [{
|
|
8677
|
+
"description": "Resource UUID",
|
|
8678
|
+
"enum": null,
|
|
8679
|
+
"name": "id",
|
|
8680
|
+
"required": true,
|
|
8681
|
+
"type": "string"
|
|
8682
|
+
}, {
|
|
8683
|
+
"description": "Function test run id returned by POST /functions/{id}/test.",
|
|
8684
|
+
"enum": null,
|
|
8685
|
+
"name": "run_id",
|
|
8686
|
+
"required": true,
|
|
8687
|
+
"type": "string"
|
|
8688
|
+
}],
|
|
8689
|
+
"queryParams": [],
|
|
8690
|
+
"requestSchema": null,
|
|
8691
|
+
"responseSchema": {
|
|
8692
|
+
"type": "object",
|
|
8693
|
+
"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",
|
|
8694
|
+
"properties": {
|
|
8695
|
+
"state": {
|
|
8696
|
+
"type": "string",
|
|
8697
|
+
"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",
|
|
8698
|
+
"enum": [
|
|
8699
|
+
"send_failed",
|
|
8700
|
+
"waiting_for_send",
|
|
8701
|
+
"waiting_for_inbound",
|
|
8702
|
+
"waiting_for_function",
|
|
8703
|
+
"completed",
|
|
8704
|
+
"failed"
|
|
8705
|
+
]
|
|
8706
|
+
},
|
|
8707
|
+
"test_run": {
|
|
8708
|
+
"type": "object",
|
|
8709
|
+
"properties": {
|
|
8710
|
+
"id": {
|
|
8711
|
+
"type": "string",
|
|
8712
|
+
"format": "uuid"
|
|
8713
|
+
},
|
|
8714
|
+
"function_id": {
|
|
8715
|
+
"type": "string",
|
|
8716
|
+
"format": "uuid"
|
|
8717
|
+
},
|
|
8718
|
+
"inbound_domain": { "type": "string" },
|
|
8719
|
+
"to": { "type": "string" },
|
|
8720
|
+
"from": { "type": "string" },
|
|
8721
|
+
"subject": { "type": "string" },
|
|
8722
|
+
"poll_since": {
|
|
8723
|
+
"type": "string",
|
|
8724
|
+
"format": "date-time"
|
|
8725
|
+
},
|
|
8726
|
+
"created_at": {
|
|
8727
|
+
"type": "string",
|
|
8728
|
+
"format": "date-time"
|
|
8729
|
+
},
|
|
8730
|
+
"sent_at": {
|
|
8731
|
+
"type": ["string", "null"],
|
|
8732
|
+
"format": "date-time"
|
|
8733
|
+
},
|
|
8734
|
+
"send_error": { "type": ["string", "null"] }
|
|
8735
|
+
},
|
|
8736
|
+
"required": [
|
|
8737
|
+
"id",
|
|
8738
|
+
"function_id",
|
|
8739
|
+
"inbound_domain",
|
|
8740
|
+
"to",
|
|
8741
|
+
"from",
|
|
8742
|
+
"subject",
|
|
8743
|
+
"poll_since",
|
|
8744
|
+
"created_at",
|
|
8745
|
+
"sent_at",
|
|
8746
|
+
"send_error"
|
|
8747
|
+
]
|
|
8748
|
+
},
|
|
8749
|
+
"test_send": {
|
|
8750
|
+
"type": ["object", "null"],
|
|
8751
|
+
"properties": {
|
|
8752
|
+
"id": {
|
|
8753
|
+
"type": "string",
|
|
8754
|
+
"format": "uuid"
|
|
8755
|
+
},
|
|
8756
|
+
"status": {
|
|
8757
|
+
"type": "string",
|
|
8758
|
+
"description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n",
|
|
8759
|
+
"enum": [
|
|
8760
|
+
"queued",
|
|
8761
|
+
"submitted_to_agent",
|
|
8762
|
+
"agent_failed",
|
|
8763
|
+
"gate_denied",
|
|
8764
|
+
"unknown",
|
|
8765
|
+
"delivered",
|
|
8766
|
+
"bounced",
|
|
8767
|
+
"deferred",
|
|
8768
|
+
"wait_timeout"
|
|
8769
|
+
]
|
|
8770
|
+
},
|
|
8771
|
+
"queue_id": { "type": ["string", "null"] },
|
|
8772
|
+
"created_at": {
|
|
8773
|
+
"type": "string",
|
|
8774
|
+
"format": "date-time"
|
|
8775
|
+
},
|
|
8776
|
+
"updated_at": {
|
|
8777
|
+
"type": "string",
|
|
8778
|
+
"format": "date-time"
|
|
8779
|
+
}
|
|
8780
|
+
},
|
|
8781
|
+
"required": [
|
|
8782
|
+
"id",
|
|
8783
|
+
"status",
|
|
8784
|
+
"queue_id",
|
|
8785
|
+
"created_at",
|
|
8786
|
+
"updated_at"
|
|
8787
|
+
]
|
|
8788
|
+
},
|
|
8789
|
+
"inbound_email": {
|
|
8790
|
+
"type": ["object", "null"],
|
|
8791
|
+
"properties": {
|
|
8792
|
+
"id": {
|
|
8793
|
+
"type": "string",
|
|
8794
|
+
"format": "uuid"
|
|
8795
|
+
},
|
|
8796
|
+
"status": {
|
|
8797
|
+
"type": "string",
|
|
8798
|
+
"description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
|
|
8799
|
+
"enum": [
|
|
8800
|
+
"pending",
|
|
8801
|
+
"accepted",
|
|
8802
|
+
"completed",
|
|
8803
|
+
"rejected"
|
|
8804
|
+
]
|
|
8805
|
+
},
|
|
8806
|
+
"received_at": {
|
|
8807
|
+
"type": "string",
|
|
8808
|
+
"format": "date-time"
|
|
8809
|
+
},
|
|
8810
|
+
"from": { "type": "string" },
|
|
8811
|
+
"to": { "type": "string" },
|
|
8812
|
+
"subject": { "type": ["string", "null"] },
|
|
8813
|
+
"webhook_status": {
|
|
8814
|
+
"type": ["string", "null"],
|
|
8815
|
+
"description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
|
|
8816
|
+
"enum": [
|
|
8817
|
+
"pending",
|
|
8818
|
+
"in_flight",
|
|
8819
|
+
"fired",
|
|
8820
|
+
"failed",
|
|
8821
|
+
"exhausted",
|
|
8822
|
+
null
|
|
8823
|
+
]
|
|
8824
|
+
},
|
|
8825
|
+
"webhook_attempt_count": { "type": "integer" },
|
|
8826
|
+
"webhook_last_status_code": { "type": ["integer", "null"] },
|
|
8827
|
+
"webhook_last_error": { "type": ["string", "null"] }
|
|
8828
|
+
},
|
|
8829
|
+
"required": [
|
|
8830
|
+
"id",
|
|
8831
|
+
"status",
|
|
8832
|
+
"received_at",
|
|
8833
|
+
"from",
|
|
8834
|
+
"to",
|
|
8835
|
+
"subject",
|
|
8836
|
+
"webhook_status",
|
|
8837
|
+
"webhook_attempt_count",
|
|
8838
|
+
"webhook_last_status_code",
|
|
8839
|
+
"webhook_last_error"
|
|
8840
|
+
]
|
|
8841
|
+
},
|
|
8842
|
+
"deliveries": {
|
|
8843
|
+
"type": "array",
|
|
8844
|
+
"items": {
|
|
8845
|
+
"type": "object",
|
|
8846
|
+
"properties": {
|
|
8847
|
+
"id": {
|
|
8848
|
+
"type": "string",
|
|
8849
|
+
"description": "Webhook delivery id."
|
|
8850
|
+
},
|
|
8851
|
+
"endpoint_id": {
|
|
8852
|
+
"type": "string",
|
|
8853
|
+
"format": "uuid"
|
|
8854
|
+
},
|
|
8855
|
+
"endpoint_url": {
|
|
8856
|
+
"type": "string",
|
|
8857
|
+
"format": "uri"
|
|
8858
|
+
},
|
|
8859
|
+
"status": {
|
|
8860
|
+
"type": "string",
|
|
8861
|
+
"enum": [
|
|
8862
|
+
"pending",
|
|
8863
|
+
"delivered",
|
|
8864
|
+
"header_confirmed",
|
|
8865
|
+
"failed"
|
|
8866
|
+
]
|
|
8867
|
+
},
|
|
8868
|
+
"attempt_count": { "type": "integer" },
|
|
8869
|
+
"duration_ms": { "type": ["integer", "null"] },
|
|
8870
|
+
"last_error": { "type": ["string", "null"] },
|
|
8871
|
+
"last_error_code": { "type": ["string", "null"] },
|
|
8872
|
+
"created_at": {
|
|
8873
|
+
"type": "string",
|
|
8874
|
+
"format": "date-time"
|
|
8875
|
+
},
|
|
8876
|
+
"updated_at": {
|
|
8877
|
+
"type": "string",
|
|
8878
|
+
"format": "date-time"
|
|
8879
|
+
},
|
|
8880
|
+
"endpoint": {
|
|
8881
|
+
"type": ["object", "null"],
|
|
8882
|
+
"properties": {
|
|
8883
|
+
"id": {
|
|
8884
|
+
"type": "string",
|
|
8885
|
+
"format": "uuid"
|
|
8886
|
+
},
|
|
8887
|
+
"kind": {
|
|
8888
|
+
"type": "string",
|
|
8889
|
+
"description": "Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear."
|
|
8890
|
+
},
|
|
8891
|
+
"function_id": {
|
|
8892
|
+
"type": ["string", "null"],
|
|
8893
|
+
"format": "uuid"
|
|
8894
|
+
},
|
|
8895
|
+
"function_name": { "type": ["string", "null"] },
|
|
8896
|
+
"domain_id": {
|
|
8897
|
+
"type": ["string", "null"],
|
|
8898
|
+
"format": "uuid"
|
|
8899
|
+
},
|
|
8900
|
+
"enabled": { "type": "boolean" },
|
|
8901
|
+
"deactivated_at": {
|
|
8902
|
+
"type": ["string", "null"],
|
|
8903
|
+
"format": "date-time"
|
|
8904
|
+
},
|
|
8905
|
+
"is_current_function": { "type": "boolean" }
|
|
8906
|
+
},
|
|
8907
|
+
"required": [
|
|
8908
|
+
"id",
|
|
8909
|
+
"kind",
|
|
8910
|
+
"function_id",
|
|
8911
|
+
"function_name",
|
|
8912
|
+
"domain_id",
|
|
8913
|
+
"enabled",
|
|
8914
|
+
"deactivated_at",
|
|
8915
|
+
"is_current_function"
|
|
8916
|
+
]
|
|
8917
|
+
}
|
|
8918
|
+
},
|
|
8919
|
+
"required": [
|
|
8920
|
+
"id",
|
|
8921
|
+
"endpoint_id",
|
|
8922
|
+
"endpoint_url",
|
|
8923
|
+
"status",
|
|
8924
|
+
"attempt_count",
|
|
8925
|
+
"duration_ms",
|
|
8926
|
+
"last_error",
|
|
8927
|
+
"last_error_code",
|
|
8928
|
+
"created_at",
|
|
8929
|
+
"updated_at",
|
|
8930
|
+
"endpoint"
|
|
8931
|
+
]
|
|
8932
|
+
}
|
|
8933
|
+
},
|
|
8934
|
+
"outbound_requests": {
|
|
8935
|
+
"type": "array",
|
|
8936
|
+
"items": {
|
|
8937
|
+
"type": "object",
|
|
8938
|
+
"properties": {
|
|
8939
|
+
"id": {
|
|
8940
|
+
"type": "string",
|
|
8941
|
+
"format": "uuid"
|
|
8942
|
+
},
|
|
8943
|
+
"function_id": {
|
|
8944
|
+
"type": "string",
|
|
8945
|
+
"format": "uuid"
|
|
8946
|
+
},
|
|
8947
|
+
"webhook_delivery_id": { "type": ["string", "null"] },
|
|
8948
|
+
"email_id": {
|
|
8949
|
+
"type": ["string", "null"],
|
|
8950
|
+
"format": "uuid"
|
|
8951
|
+
},
|
|
8952
|
+
"endpoint_id": {
|
|
8953
|
+
"type": ["string", "null"],
|
|
8954
|
+
"format": "uuid"
|
|
8955
|
+
},
|
|
8956
|
+
"method": { "type": "string" },
|
|
8957
|
+
"url": {
|
|
8958
|
+
"type": "string",
|
|
8959
|
+
"format": "uri"
|
|
8960
|
+
},
|
|
8961
|
+
"host": { "type": "string" },
|
|
8962
|
+
"path": { "type": "string" },
|
|
8963
|
+
"status_code": { "type": ["integer", "null"] },
|
|
8964
|
+
"ok": { "type": ["boolean", "null"] },
|
|
8965
|
+
"duration_ms": { "type": "integer" },
|
|
8966
|
+
"error": { "type": ["string", "null"] },
|
|
8967
|
+
"ts": {
|
|
8968
|
+
"type": "string",
|
|
8969
|
+
"format": "date-time"
|
|
8970
|
+
}
|
|
8971
|
+
},
|
|
8972
|
+
"required": [
|
|
8973
|
+
"id",
|
|
8974
|
+
"function_id",
|
|
8975
|
+
"webhook_delivery_id",
|
|
8976
|
+
"email_id",
|
|
8977
|
+
"endpoint_id",
|
|
8978
|
+
"method",
|
|
8979
|
+
"url",
|
|
8980
|
+
"host",
|
|
8981
|
+
"path",
|
|
8982
|
+
"status_code",
|
|
8983
|
+
"ok",
|
|
8984
|
+
"duration_ms",
|
|
8985
|
+
"error",
|
|
8986
|
+
"ts"
|
|
8987
|
+
]
|
|
8988
|
+
}
|
|
8989
|
+
},
|
|
8990
|
+
"logs": {
|
|
8991
|
+
"type": "array",
|
|
8992
|
+
"items": {
|
|
8993
|
+
"type": "object",
|
|
8994
|
+
"description": "One row from GET /functions/{id}/logs. Represents a single\ncaptured log line emitted by the running handler (e.g. via\n`console.log` / `console.error`).\n",
|
|
8995
|
+
"properties": {
|
|
8996
|
+
"id": {
|
|
8997
|
+
"type": "string",
|
|
8998
|
+
"format": "uuid",
|
|
8999
|
+
"description": "Unique log row id (stable across pages)."
|
|
9000
|
+
},
|
|
9001
|
+
"function_id": {
|
|
9002
|
+
"type": "string",
|
|
9003
|
+
"format": "uuid",
|
|
9004
|
+
"description": "The function this log row belongs to."
|
|
9005
|
+
},
|
|
9006
|
+
"level": {
|
|
9007
|
+
"type": "string",
|
|
9008
|
+
"enum": [
|
|
9009
|
+
"debug",
|
|
9010
|
+
"log",
|
|
9011
|
+
"info",
|
|
9012
|
+
"warn",
|
|
9013
|
+
"error"
|
|
9014
|
+
],
|
|
9015
|
+
"description": "Severity. `log` is the runtime's default for unannotated\n`console.log` calls; the other levels match standard\n`console.*` methods.\n"
|
|
9016
|
+
},
|
|
9017
|
+
"message": {
|
|
9018
|
+
"type": "string",
|
|
9019
|
+
"description": "The textual message body. The runtime stringifies non-string\narguments before persisting, so this is always a plain\nstring.\n"
|
|
9020
|
+
},
|
|
9021
|
+
"ts": {
|
|
9022
|
+
"type": "string",
|
|
9023
|
+
"format": "date-time",
|
|
9024
|
+
"description": "When the handler emitted this line. Newest-first ordering\non this column drives pagination; clock is the runtime's,\nnot the gateway's.\n"
|
|
9025
|
+
},
|
|
9026
|
+
"metadata": {
|
|
9027
|
+
"type": ["object", "null"],
|
|
9028
|
+
"additionalProperties": true,
|
|
9029
|
+
"description": "Optional structured payload the runtime attaches alongside\nthe message (e.g. extra args passed to `console.log`).\nShape is opaque; treat keys as untyped.\n"
|
|
9030
|
+
}
|
|
9031
|
+
},
|
|
9032
|
+
"required": [
|
|
9033
|
+
"id",
|
|
9034
|
+
"function_id",
|
|
9035
|
+
"level",
|
|
9036
|
+
"message",
|
|
9037
|
+
"ts"
|
|
9038
|
+
]
|
|
9039
|
+
}
|
|
9040
|
+
},
|
|
9041
|
+
"replies": {
|
|
9042
|
+
"type": "array",
|
|
9043
|
+
"items": {
|
|
9044
|
+
"type": "object",
|
|
9045
|
+
"properties": {
|
|
9046
|
+
"id": {
|
|
9047
|
+
"type": "string",
|
|
9048
|
+
"format": "uuid"
|
|
9049
|
+
},
|
|
9050
|
+
"status": {
|
|
9051
|
+
"type": "string",
|
|
9052
|
+
"description": "Lifecycle status of a sent_emails row. Possible values:\n\n - `queued`: pre-call INSERT; the outbound agent has not\n yet replied.\n - `submitted_to_agent`: agent accepted; `queue_id` is set.\n - `agent_failed`: agent rejected; `error_code` and\n `error_message` carry the reason.\n - `gate_denied`: a recipient-scope gate denied the send;\n the agent was never called. The `gates` array carries\n the denial detail. /send-mail returns 403 in this case\n so callers see the denial synchronously; /sent-emails\n additionally records the row for historical lookup,\n which is when this status appears in a listing.\n - `unknown`: terminal indeterminate; the on-box log\n poller couldn't classify the receiver's response.\n - `delivered` / `bounced` / `deferred` / `wait_timeout`:\n terminal delivery outcomes (see DeliveryStatus).\n",
|
|
9053
|
+
"enum": [
|
|
9054
|
+
"queued",
|
|
9055
|
+
"submitted_to_agent",
|
|
9056
|
+
"agent_failed",
|
|
9057
|
+
"gate_denied",
|
|
9058
|
+
"unknown",
|
|
9059
|
+
"delivered",
|
|
9060
|
+
"bounced",
|
|
9061
|
+
"deferred",
|
|
9062
|
+
"wait_timeout"
|
|
9063
|
+
]
|
|
9064
|
+
},
|
|
9065
|
+
"to": { "type": "string" },
|
|
9066
|
+
"subject": { "type": "string" },
|
|
9067
|
+
"queue_id": { "type": ["string", "null"] },
|
|
9068
|
+
"created_at": {
|
|
9069
|
+
"type": "string",
|
|
9070
|
+
"format": "date-time"
|
|
9071
|
+
}
|
|
9072
|
+
},
|
|
9073
|
+
"required": [
|
|
9074
|
+
"id",
|
|
9075
|
+
"status",
|
|
9076
|
+
"to",
|
|
9077
|
+
"subject",
|
|
9078
|
+
"queue_id",
|
|
9079
|
+
"created_at"
|
|
9080
|
+
]
|
|
9081
|
+
}
|
|
9082
|
+
}
|
|
9083
|
+
},
|
|
9084
|
+
"required": [
|
|
9085
|
+
"state",
|
|
9086
|
+
"test_run",
|
|
9087
|
+
"test_send",
|
|
9088
|
+
"inbound_email",
|
|
9089
|
+
"deliveries",
|
|
9090
|
+
"outbound_requests",
|
|
9091
|
+
"logs",
|
|
9092
|
+
"replies"
|
|
7755
9093
|
]
|
|
7756
9094
|
},
|
|
7757
|
-
"sdkName": "
|
|
7758
|
-
"summary": "Get a function",
|
|
9095
|
+
"sdkName": "getFunctionTestRunTrace",
|
|
9096
|
+
"summary": "Get a function test run trace",
|
|
7759
9097
|
"tag": "Functions",
|
|
7760
9098
|
"tagCommand": "functions"
|
|
7761
9099
|
},
|
|
@@ -8075,8 +9413,13 @@ const operationManifest = [
|
|
|
8075
9413
|
},
|
|
8076
9414
|
"responseSchema": {
|
|
8077
9415
|
"type": "object",
|
|
8078
|
-
"description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued;
|
|
9416
|
+
"description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; poll `trace_url` to watch the run progress through\nsend -> inbound -> webhook deliveries -> outbound requests,\nlogs, and replies.\n",
|
|
8079
9417
|
"properties": {
|
|
9418
|
+
"test_run_id": {
|
|
9419
|
+
"type": "string",
|
|
9420
|
+
"format": "uuid",
|
|
9421
|
+
"description": "Durable test run id used to fetch the run trace."
|
|
9422
|
+
},
|
|
8080
9423
|
"inbound_domain": {
|
|
8081
9424
|
"type": "string",
|
|
8082
9425
|
"description": "Verified inbound domain the test email was sent to."
|
|
@@ -8106,16 +9449,22 @@ const operationManifest = [
|
|
|
8106
9449
|
"type": "string",
|
|
8107
9450
|
"format": "uri",
|
|
8108
9451
|
"description": "Function detail page where invocations show up live."
|
|
9452
|
+
},
|
|
9453
|
+
"trace_url": {
|
|
9454
|
+
"type": "string",
|
|
9455
|
+
"description": "Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace."
|
|
8109
9456
|
}
|
|
8110
9457
|
},
|
|
8111
9458
|
"required": [
|
|
9459
|
+
"test_run_id",
|
|
8112
9460
|
"inbound_domain",
|
|
8113
9461
|
"to",
|
|
8114
9462
|
"from",
|
|
8115
9463
|
"send_id",
|
|
8116
9464
|
"subject",
|
|
8117
9465
|
"poll_since",
|
|
8118
|
-
"watch_url"
|
|
9466
|
+
"watch_url",
|
|
9467
|
+
"trace_url"
|
|
8119
9468
|
]
|
|
8120
9469
|
},
|
|
8121
9470
|
"sdkName": "testFunction",
|
|
@@ -8127,7 +9476,7 @@ const operationManifest = [
|
|
|
8127
9476
|
"binaryResponse": false,
|
|
8128
9477
|
"bodyRequired": true,
|
|
8129
9478
|
"command": "update-function",
|
|
8130
|
-
"description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn
|
|
9479
|
+
"description": "Replaces the function's source code with the body's `code` and\ntriggers a redeploy. Same size limits as `POST /functions`.\nUse this verb to push secret writes into the running handler:\npassing the same `code` re-runs the deploy and refreshes the\nbinding set with the latest values from the secrets table.\n\nOn deploy failure, the previously-deployed code stays live; the\nruntime never serves a half-built bundle. The response uses\n`error.code` `deploy_failed`, and the function's `deploy_error`\nfield carries the latest deploy error for dashboard/API reads.\n",
|
|
8131
9480
|
"hasJsonBody": true,
|
|
8132
9481
|
"method": "PUT",
|
|
8133
9482
|
"operationId": "updateFunction",
|
|
@@ -9386,7 +10735,7 @@ const CREDENTIALS_FILE = "credentials.json";
|
|
|
9386
10735
|
const CREDENTIALS_LOCK_DIR = "credentials.lock";
|
|
9387
10736
|
const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
|
|
9388
10737
|
const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`.";
|
|
9389
|
-
function isRecord(value) {
|
|
10738
|
+
function isRecord$1(value) {
|
|
9390
10739
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9391
10740
|
}
|
|
9392
10741
|
function requireString(value, key) {
|
|
@@ -9409,7 +10758,7 @@ var StaleCredentialFormatError = class extends Error {
|
|
|
9409
10758
|
}
|
|
9410
10759
|
};
|
|
9411
10760
|
function parseCredentials(raw) {
|
|
9412
|
-
if (!isRecord(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
10761
|
+
if (!isRecord$1(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
9413
10762
|
if (typeof raw.api_base_url_1 !== "string" && typeof raw.base_url === "string") throw new StaleCredentialFormatError();
|
|
9414
10763
|
const orgName = raw.org_name;
|
|
9415
10764
|
if (orgName !== null && typeof orgName !== "string") throw new Error(`Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
@@ -9582,7 +10931,7 @@ function formatFunctionEndpointRedirect(match) {
|
|
|
9582
10931
|
return [
|
|
9583
10932
|
"This is a function endpoint. Function endpoints are tested differently. Run:",
|
|
9584
10933
|
"",
|
|
9585
|
-
` primitive functions
|
|
10934
|
+
` primitive functions test --id ${match.functionId}`,
|
|
9586
10935
|
"",
|
|
9587
10936
|
`(pass the function id, not the endpoint id. endpoint_id=${match.endpointId} function_id=${match.functionId})`
|
|
9588
10937
|
].join("\n");
|
|
@@ -9753,26 +11102,26 @@ function coerceParameterValue(parameter, value) {
|
|
|
9753
11102
|
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
|
|
9754
11103
|
throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
|
|
9755
11104
|
}
|
|
9756
|
-
function cliError$
|
|
11105
|
+
function cliError$5(message) {
|
|
9757
11106
|
return new Errors.CLIError(message, { exit: 1 });
|
|
9758
11107
|
}
|
|
9759
11108
|
function parseJson(source, flagLabel) {
|
|
9760
11109
|
try {
|
|
9761
11110
|
return JSON.parse(source);
|
|
9762
11111
|
} catch (error) {
|
|
9763
|
-
throw cliError$
|
|
11112
|
+
throw cliError$5(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
9764
11113
|
}
|
|
9765
11114
|
}
|
|
9766
11115
|
function readJsonBody(flags) {
|
|
9767
11116
|
const bodyFile = flags["body-file"];
|
|
9768
11117
|
const rawBody = flags["raw-body"];
|
|
9769
|
-
if (bodyFile && rawBody) throw cliError$
|
|
11118
|
+
if (bodyFile && rawBody) throw cliError$5("Use either --raw-body or --body-file, not both");
|
|
9770
11119
|
if (typeof bodyFile === "string") {
|
|
9771
11120
|
let contents;
|
|
9772
11121
|
try {
|
|
9773
11122
|
contents = readFileSync(bodyFile, "utf8");
|
|
9774
11123
|
} catch (error) {
|
|
9775
|
-
throw cliError$
|
|
11124
|
+
throw cliError$5(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9776
11125
|
}
|
|
9777
11126
|
return parseJson(contents, `--body-file ${bodyFile}`);
|
|
9778
11127
|
}
|
|
@@ -9782,7 +11131,7 @@ function readTextFileFlag(path, flagLabel) {
|
|
|
9782
11131
|
try {
|
|
9783
11132
|
return readFileSync(path, "utf8");
|
|
9784
11133
|
} catch (error) {
|
|
9785
|
-
throw cliError$
|
|
11134
|
+
throw cliError$5(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9786
11135
|
}
|
|
9787
11136
|
}
|
|
9788
11137
|
function extractErrorPayload(raw) {
|
|
@@ -9954,14 +11303,14 @@ function collectValues(parameters, flags) {
|
|
|
9954
11303
|
return values;
|
|
9955
11304
|
}
|
|
9956
11305
|
const OPERATION_HINTS = {
|
|
9957
|
-
createFunction: "Tip: prefer `primitive functions
|
|
9958
|
-
updateFunction: "Tip: prefer `primitive functions
|
|
9959
|
-
createFunctionSecret: "Tip: prefer `primitive functions
|
|
9960
|
-
setFunctionSecret: "Tip: prefer `primitive functions
|
|
11306
|
+
createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
11307
|
+
updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
|
|
11308
|
+
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.",
|
|
11309
|
+
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."
|
|
9961
11310
|
};
|
|
9962
11311
|
function createOperationCommand(operation) {
|
|
9963
11312
|
const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
|
|
9964
|
-
const baseDescription = operation.description
|
|
11313
|
+
const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
|
|
9965
11314
|
const schemaSummary = operation.hasJsonBody ? renderRequestSchemaSummary(operation.requestSchema) : null;
|
|
9966
11315
|
const hint = OPERATION_HINTS[operation.sdkName];
|
|
9967
11316
|
const descriptionWithSchema = schemaSummary ? `${baseDescription}\n\n${schemaSummary}` : baseDescription;
|
|
@@ -10067,12 +11416,15 @@ function createOperationCommand(operation) {
|
|
|
10067
11416
|
return OperationCommand;
|
|
10068
11417
|
}
|
|
10069
11418
|
const EMPTY_RESULT_HINTS = {
|
|
10070
|
-
listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints
|
|
10071
|
-
listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints
|
|
10072
|
-
listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails
|
|
10073
|
-
listDomains: "(no results) No domains on this account. Add one with `primitive domains
|
|
11419
|
+
listDeliveries: "(no results) No webhook deliveries logged yet. If you have an endpoint configured but expected to see test fires here: test deliveries from `primitive endpoints test` are NOT logged in this list, they're synchronous and visible only in the test-endpoint command's response. Real deliveries are logged when an inbound `email.received` event fans out to your endpoints. If you have no endpoints, run `primitive endpoints list` to check.",
|
|
11420
|
+
listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
|
|
11421
|
+
listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
|
|
11422
|
+
listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
|
|
10074
11423
|
listFilters: "(no results) No filter rules configured."
|
|
10075
11424
|
};
|
|
11425
|
+
function canonicalizeCliReferences(description) {
|
|
11426
|
+
return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
|
|
11427
|
+
}
|
|
10076
11428
|
//#endregion
|
|
10077
11429
|
//#region src/oclif/commands/doctor.ts
|
|
10078
11430
|
const MIN_NODE_MAJOR = 22;
|
|
@@ -10233,7 +11585,7 @@ async function checkDomains(opts) {
|
|
|
10233
11585
|
if (result.error) return {
|
|
10234
11586
|
status: "warn",
|
|
10235
11587
|
message: "could not list domains",
|
|
10236
|
-
hint: "Run `primitive domains
|
|
11588
|
+
hint: "Run `primitive domains list` for the full error envelope."
|
|
10237
11589
|
};
|
|
10238
11590
|
const rows = result.data?.data ?? [];
|
|
10239
11591
|
const active = rows.filter((row) => row.is_active === true);
|
|
@@ -10245,7 +11597,7 @@ async function checkDomains(opts) {
|
|
|
10245
11597
|
if (active.length === 0) return {
|
|
10246
11598
|
status: "warn",
|
|
10247
11599
|
message: `${rows.length} domain(s), none active`,
|
|
10248
|
-
hint: "Run `primitive domains
|
|
11600
|
+
hint: "Run `primitive domains verify --id <id>` for any domain you intend to send / receive on."
|
|
10249
11601
|
};
|
|
10250
11602
|
return {
|
|
10251
11603
|
status: "ok",
|
|
@@ -10374,11 +11726,11 @@ function formatHeader(idWidth) {
|
|
|
10374
11726
|
return `${"ID".padEnd(idWidth)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
|
|
10375
11727
|
}
|
|
10376
11728
|
var EmailsLatestCommand = class EmailsLatestCommand extends Command {
|
|
10377
|
-
static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails
|
|
11729
|
+
static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails list\` (full JSON envelope, cursor pagination, filters) or pass \`--json\` here for the same raw shape without pagination/filters.
|
|
10378
11730
|
|
|
10379
11731
|
ID display is TTY-aware. When STDOUT is a terminal, the table truncates each row's id to the first ${ID_DISPLAY_WIDTH_SHORT} characters for readability. When STDOUT is piped or redirected (the row stream is being consumed by another command), the full UUID is printed so the id can be fed straight back into \`emails:get-email\`, \`emails:delete-email\`, etc. without a separate \`--json\` round-trip.
|
|
10380
11732
|
|
|
10381
|
-
Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT and is equivalent to running \`emails
|
|
11733
|
+
Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT and is equivalent to running \`emails list --limit N\` for the same N.`;
|
|
10382
11734
|
static summary = "Show the most recent inbound emails as a compact table";
|
|
10383
11735
|
static examples = [
|
|
10384
11736
|
"<%= config.bin %> emails latest",
|
|
@@ -10558,7 +11910,7 @@ function sleep$1(ms) {
|
|
|
10558
11910
|
//#endregion
|
|
10559
11911
|
//#region src/oclif/commands/emails-wait.ts
|
|
10560
11912
|
const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
|
|
10561
|
-
function cliError$
|
|
11913
|
+
function cliError$4(message) {
|
|
10562
11914
|
return new Errors.CLIError(message, { exit: 1 });
|
|
10563
11915
|
}
|
|
10564
11916
|
var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
@@ -10638,7 +11990,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
10638
11990
|
try {
|
|
10639
11991
|
since = sinceFromFlags(flags);
|
|
10640
11992
|
} catch (error) {
|
|
10641
|
-
throw cliError$
|
|
11993
|
+
throw cliError$4(error instanceof Error ? error.message : String(error));
|
|
10642
11994
|
}
|
|
10643
11995
|
const filters = filtersFromFlags(flags);
|
|
10644
11996
|
const deadline = flags.timeout === 0 ? null : Date.now() + flags.timeout * 1e3;
|
|
@@ -10689,7 +12041,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
|
|
|
10689
12041
|
};
|
|
10690
12042
|
//#endregion
|
|
10691
12043
|
//#region src/oclif/commands/emails-watch.ts
|
|
10692
|
-
function cliError$
|
|
12044
|
+
function cliError$3(message) {
|
|
10693
12045
|
return new Errors.CLIError(message, { exit: 1 });
|
|
10694
12046
|
}
|
|
10695
12047
|
var EmailsWatchCommand = class EmailsWatchCommand extends Command {
|
|
@@ -10766,7 +12118,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
|
|
|
10766
12118
|
try {
|
|
10767
12119
|
since = sinceFromFlags(flags);
|
|
10768
12120
|
} catch (error) {
|
|
10769
|
-
throw cliError$
|
|
12121
|
+
throw cliError$3(error instanceof Error ? error.message : String(error));
|
|
10770
12122
|
}
|
|
10771
12123
|
const filters = filtersFromFlags(flags);
|
|
10772
12124
|
const deadline = flags.seconds ? Date.now() + flags.seconds * 1e3 : null;
|
|
@@ -10852,41 +12204,319 @@ function emitRawSendMailFetchWarning(bundleText, write) {
|
|
|
10852
12204
|
//#endregion
|
|
10853
12205
|
//#region src/oclif/secret-flags.ts
|
|
10854
12206
|
const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
10855
|
-
function
|
|
12207
|
+
function resolveSecretFlags(input) {
|
|
10856
12208
|
const secrets = [];
|
|
10857
12209
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
12210
|
+
const env = input.env ?? process.env;
|
|
12211
|
+
const readFile = input.readFile ?? defaultReadFile;
|
|
12212
|
+
const readStdin = input.readStdin ?? defaultReadStdin;
|
|
12213
|
+
const envFileCache = /* @__PURE__ */ new Map();
|
|
12214
|
+
const reserveSecretKey = (key, sourceLabel) => {
|
|
12215
|
+
const keyError = validateKey(key, sourceLabel);
|
|
12216
|
+
if (keyError) return keyError;
|
|
12217
|
+
if (seenKeys.has(key)) return duplicateKeyError(key);
|
|
12218
|
+
seenKeys.add(key);
|
|
12219
|
+
return null;
|
|
12220
|
+
};
|
|
12221
|
+
const addSecret = (key, value, sourceLabel) => {
|
|
12222
|
+
const keyError = reserveSecretKey(key, sourceLabel);
|
|
12223
|
+
if (keyError) return keyError;
|
|
12224
|
+
secrets.push({
|
|
12225
|
+
key,
|
|
12226
|
+
value
|
|
12227
|
+
});
|
|
12228
|
+
return null;
|
|
12229
|
+
};
|
|
12230
|
+
for (const entry of input.inline ?? []) {
|
|
12231
|
+
const parsed = parseKeyValueFlag(entry, "--secret");
|
|
12232
|
+
if (parsed.kind === "error") return parsed;
|
|
12233
|
+
const error = addSecret(parsed.key, parsed.value, "--secret");
|
|
12234
|
+
if (error) return error;
|
|
12235
|
+
}
|
|
12236
|
+
for (const key of input.fromEnv ?? []) {
|
|
12237
|
+
const keyError = reserveSecretKey(key, "--secret-from-env");
|
|
12238
|
+
if (keyError) return keyError;
|
|
12239
|
+
const value = env[key];
|
|
12240
|
+
if (value === void 0) return {
|
|
10861
12241
|
kind: "error",
|
|
10862
|
-
message: `--secret
|
|
12242
|
+
message: `--secret-from-env ${key} could not read ${key}: environment variable is not set.`
|
|
10863
12243
|
};
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
12244
|
+
secrets.push({
|
|
12245
|
+
key,
|
|
12246
|
+
value
|
|
12247
|
+
});
|
|
12248
|
+
}
|
|
12249
|
+
for (const entry of input.fromFile ?? []) {
|
|
12250
|
+
const parsed = parseKeyValueFlag(entry, "--secret-from-file");
|
|
12251
|
+
if (parsed.kind === "error") return parsed;
|
|
12252
|
+
const keyError = reserveSecretKey(parsed.key, "--secret-from-file");
|
|
12253
|
+
if (keyError) return keyError;
|
|
12254
|
+
const file = readSecretFile(parsed.value, "--secret-from-file", readFile);
|
|
12255
|
+
if (file.kind === "error") return file;
|
|
12256
|
+
secrets.push({
|
|
12257
|
+
key: parsed.key,
|
|
12258
|
+
value: file.value
|
|
12259
|
+
});
|
|
12260
|
+
}
|
|
12261
|
+
for (const entry of input.fromEnvFile ?? []) {
|
|
12262
|
+
const parsed = parseEnvFileKeyRef(entry, "--secret-from-env-file");
|
|
12263
|
+
if (parsed.kind === "error") return parsed;
|
|
12264
|
+
const keyError = reserveSecretKey(parsed.key, "--secret-from-env-file");
|
|
12265
|
+
if (keyError) return keyError;
|
|
12266
|
+
const file = readEnvFile(parsed.path, readFile, envFileCache);
|
|
12267
|
+
if (file.kind === "error") return file;
|
|
12268
|
+
const value = file.values.get(parsed.key);
|
|
12269
|
+
if (value === void 0) return {
|
|
10867
12270
|
kind: "error",
|
|
10868
|
-
message: `--secret
|
|
12271
|
+
message: `--secret-from-env-file ${entry} could not read ${parsed.key}: key is not present in ${parsed.path}.`
|
|
10869
12272
|
};
|
|
10870
|
-
|
|
12273
|
+
secrets.push({
|
|
12274
|
+
key: parsed.key,
|
|
12275
|
+
value
|
|
12276
|
+
});
|
|
12277
|
+
}
|
|
12278
|
+
if (input.fromStdin !== void 0) {
|
|
12279
|
+
const keyError = reserveSecretKey(input.fromStdin, "--secret-from-stdin");
|
|
12280
|
+
if (keyError) return keyError;
|
|
12281
|
+
const stdin = readSecretStdin("--secret-from-stdin", readStdin);
|
|
12282
|
+
if (stdin.kind === "error") return stdin;
|
|
12283
|
+
secrets.push({
|
|
12284
|
+
key: input.fromStdin,
|
|
12285
|
+
value: stdin.value
|
|
12286
|
+
});
|
|
12287
|
+
}
|
|
12288
|
+
return {
|
|
12289
|
+
kind: "ok",
|
|
12290
|
+
secrets
|
|
12291
|
+
};
|
|
12292
|
+
}
|
|
12293
|
+
function resolveSingleSecretValue(input) {
|
|
12294
|
+
if ([
|
|
12295
|
+
input.value !== void 0 ? "--value" : null,
|
|
12296
|
+
input.valueFromEnv !== void 0 ? "--value-from-env" : null,
|
|
12297
|
+
input.valueFile !== void 0 ? "--value-file" : null,
|
|
12298
|
+
input.valueFromEnvFile !== void 0 ? "--value-from-env-file" : null,
|
|
12299
|
+
input.stdin === true ? "--stdin" : null
|
|
12300
|
+
].filter((v) => v !== null).length !== 1) return {
|
|
12301
|
+
kind: "error",
|
|
12302
|
+
message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
|
|
12303
|
+
};
|
|
12304
|
+
const env = input.env ?? process.env;
|
|
12305
|
+
const readFile = input.readFile ?? defaultReadFile;
|
|
12306
|
+
const readStdin = input.readStdin ?? defaultReadStdin;
|
|
12307
|
+
if (input.value !== void 0) return {
|
|
12308
|
+
kind: "ok",
|
|
12309
|
+
value: input.value
|
|
12310
|
+
};
|
|
12311
|
+
if (input.valueFromEnv !== void 0) {
|
|
12312
|
+
const value = env[input.valueFromEnv];
|
|
12313
|
+
if (value === void 0) return {
|
|
10871
12314
|
kind: "error",
|
|
10872
|
-
message: `--
|
|
12315
|
+
message: `--value-from-env ${input.valueFromEnv} could not read ${input.valueFromEnv}: environment variable is not set.`
|
|
12316
|
+
};
|
|
12317
|
+
return {
|
|
12318
|
+
kind: "ok",
|
|
12319
|
+
value
|
|
10873
12320
|
};
|
|
10874
|
-
|
|
12321
|
+
}
|
|
12322
|
+
if (input.valueFile !== void 0) return readSecretFile(input.valueFile, "--value-file", readFile);
|
|
12323
|
+
if (input.valueFromEnvFile !== void 0) {
|
|
12324
|
+
const parsed = parseSingleValueEnvFileRef(input.valueFromEnvFile, input.key, "--value-from-env-file");
|
|
12325
|
+
if (parsed.kind === "error") return parsed;
|
|
12326
|
+
const file = readEnvFile(parsed.path, readFile, /* @__PURE__ */ new Map());
|
|
12327
|
+
if (file.kind === "error") return file;
|
|
12328
|
+
const value = file.values.get(parsed.key);
|
|
12329
|
+
if (value === void 0) return {
|
|
10875
12330
|
kind: "error",
|
|
10876
|
-
message: `--
|
|
12331
|
+
message: `--value-from-env-file ${input.valueFromEnvFile} could not read ${parsed.key}: key is not present in ${parsed.path}.`
|
|
10877
12332
|
};
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
key,
|
|
12333
|
+
return {
|
|
12334
|
+
kind: "ok",
|
|
10881
12335
|
value
|
|
10882
|
-
}
|
|
12336
|
+
};
|
|
10883
12337
|
}
|
|
12338
|
+
if (input.stdin === true) return readSecretStdin("--stdin", readStdin);
|
|
12339
|
+
return {
|
|
12340
|
+
kind: "error",
|
|
12341
|
+
message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
|
|
12342
|
+
};
|
|
12343
|
+
}
|
|
12344
|
+
function defaultReadFile(path) {
|
|
12345
|
+
return readFileSync(path, "utf8");
|
|
12346
|
+
}
|
|
12347
|
+
function defaultReadStdin() {
|
|
12348
|
+
if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/env source instead.");
|
|
12349
|
+
return readFileSync(0, "utf8");
|
|
12350
|
+
}
|
|
12351
|
+
function parseKeyValueFlag(entry, flagLabel) {
|
|
12352
|
+
const eq = entry.indexOf("=");
|
|
12353
|
+
if (eq === -1) return {
|
|
12354
|
+
kind: "error",
|
|
12355
|
+
message: `${flagLabel} expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: ${flagLabel} API_TOKEN=abc123`
|
|
12356
|
+
};
|
|
12357
|
+
const key = entry.slice(0, eq);
|
|
12358
|
+
const value = entry.slice(eq + 1);
|
|
12359
|
+
if (key.length === 0) return {
|
|
12360
|
+
kind: "error",
|
|
12361
|
+
message: `${flagLabel} is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: ${flagLabel} API_TOKEN=abc123`
|
|
12362
|
+
};
|
|
10884
12363
|
return {
|
|
10885
12364
|
kind: "ok",
|
|
10886
|
-
|
|
12365
|
+
key,
|
|
12366
|
+
value
|
|
12367
|
+
};
|
|
12368
|
+
}
|
|
12369
|
+
function validateKey(key, flagLabel) {
|
|
12370
|
+
if (!SECRET_KEY_RE.test(key)) return {
|
|
12371
|
+
kind: "error",
|
|
12372
|
+
message: `${flagLabel} KEY ${JSON.stringify(key)} does not match ${SECRET_KEY_RE.source} (uppercase letters, digits, underscores; first character is a letter or underscore).`
|
|
12373
|
+
};
|
|
12374
|
+
return null;
|
|
12375
|
+
}
|
|
12376
|
+
function duplicateKeyError(key) {
|
|
12377
|
+
return {
|
|
12378
|
+
kind: "error",
|
|
12379
|
+
message: `Secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`
|
|
12380
|
+
};
|
|
12381
|
+
}
|
|
12382
|
+
function readSecretFile(path, flagLabel, readFile) {
|
|
12383
|
+
try {
|
|
12384
|
+
return {
|
|
12385
|
+
kind: "ok",
|
|
12386
|
+
value: readFile(path)
|
|
12387
|
+
};
|
|
12388
|
+
} catch (error) {
|
|
12389
|
+
return {
|
|
12390
|
+
kind: "error",
|
|
12391
|
+
message: `Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
12392
|
+
};
|
|
12393
|
+
}
|
|
12394
|
+
}
|
|
12395
|
+
function readSecretStdin(flagLabel, readStdin) {
|
|
12396
|
+
try {
|
|
12397
|
+
return {
|
|
12398
|
+
kind: "ok",
|
|
12399
|
+
value: stripOneTrailingLineEnding(readStdin())
|
|
12400
|
+
};
|
|
12401
|
+
} catch (error) {
|
|
12402
|
+
return {
|
|
12403
|
+
kind: "error",
|
|
12404
|
+
message: `Could not read ${flagLabel}: ${error instanceof Error ? error.message : String(error)}`
|
|
12405
|
+
};
|
|
12406
|
+
}
|
|
12407
|
+
}
|
|
12408
|
+
function stripOneTrailingLineEnding(value) {
|
|
12409
|
+
if (!value.endsWith("\n")) return value;
|
|
12410
|
+
const withoutLf = value.slice(0, -1);
|
|
12411
|
+
return withoutLf.endsWith("\r") ? withoutLf.slice(0, -1) : withoutLf;
|
|
12412
|
+
}
|
|
12413
|
+
function parseEnvFileKeyRef(entry, flagLabel) {
|
|
12414
|
+
const sep = entry.lastIndexOf(":");
|
|
12415
|
+
if (sep <= 0 || sep === entry.length - 1) return {
|
|
12416
|
+
kind: "error",
|
|
12417
|
+
message: `${flagLabel} expects FILE:KEY (got ${JSON.stringify(entry)}). Example: ${flagLabel} .env.local:OPENAI_KEY`
|
|
12418
|
+
};
|
|
12419
|
+
const path = entry.slice(0, sep);
|
|
12420
|
+
const key = entry.slice(sep + 1);
|
|
12421
|
+
const keyError = validateKey(key, flagLabel);
|
|
12422
|
+
if (keyError) return keyError;
|
|
12423
|
+
return {
|
|
12424
|
+
kind: "ok",
|
|
12425
|
+
key,
|
|
12426
|
+
path
|
|
12427
|
+
};
|
|
12428
|
+
}
|
|
12429
|
+
function parseSingleValueEnvFileRef(entry, fallbackKey, flagLabel) {
|
|
12430
|
+
const sep = entry.lastIndexOf(":");
|
|
12431
|
+
if (sep === -1) return {
|
|
12432
|
+
kind: "ok",
|
|
12433
|
+
key: fallbackKey,
|
|
12434
|
+
path: entry
|
|
12435
|
+
};
|
|
12436
|
+
if (sep <= 0 || sep === entry.length - 1) return {
|
|
12437
|
+
kind: "error",
|
|
12438
|
+
message: `${flagLabel} expects FILE or FILE:KEY (got ${JSON.stringify(entry)}). Example: ${flagLabel} .env.local or ${flagLabel} .env.local:OPENAI_KEY`
|
|
12439
|
+
};
|
|
12440
|
+
const path = entry.slice(0, sep);
|
|
12441
|
+
const key = entry.slice(sep + 1);
|
|
12442
|
+
const keyError = validateKey(key, flagLabel);
|
|
12443
|
+
if (keyError) return keyError;
|
|
12444
|
+
return {
|
|
12445
|
+
kind: "ok",
|
|
12446
|
+
key,
|
|
12447
|
+
path
|
|
12448
|
+
};
|
|
12449
|
+
}
|
|
12450
|
+
function readEnvFile(path, readFile, cache) {
|
|
12451
|
+
const cached = cache.get(path);
|
|
12452
|
+
if (cached) return {
|
|
12453
|
+
kind: "ok",
|
|
12454
|
+
values: cached
|
|
12455
|
+
};
|
|
12456
|
+
let contents;
|
|
12457
|
+
try {
|
|
12458
|
+
contents = readFile(path);
|
|
12459
|
+
} catch (error) {
|
|
12460
|
+
return {
|
|
12461
|
+
kind: "error",
|
|
12462
|
+
message: `Could not read env file ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
12463
|
+
};
|
|
12464
|
+
}
|
|
12465
|
+
const values = parseEnvFile(contents);
|
|
12466
|
+
cache.set(path, values);
|
|
12467
|
+
return {
|
|
12468
|
+
kind: "ok",
|
|
12469
|
+
values
|
|
10887
12470
|
};
|
|
10888
12471
|
}
|
|
10889
|
-
|
|
12472
|
+
function parseEnvFile(contents) {
|
|
12473
|
+
const values = /* @__PURE__ */ new Map();
|
|
12474
|
+
const normalized = contents.replace(/^\uFEFF/, "");
|
|
12475
|
+
for (const rawLine of normalized.split(/\r?\n/)) {
|
|
12476
|
+
let line = rawLine.trimStart();
|
|
12477
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
12478
|
+
if (line.startsWith("export ")) line = line.slice(7).trimStart();
|
|
12479
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
12480
|
+
if (!match) continue;
|
|
12481
|
+
values.set(match[1], parseEnvValue(match[2] ?? ""));
|
|
12482
|
+
}
|
|
12483
|
+
return values;
|
|
12484
|
+
}
|
|
12485
|
+
function parseEnvValue(raw) {
|
|
12486
|
+
const value = raw.trimStart();
|
|
12487
|
+
if (value.startsWith("'")) {
|
|
12488
|
+
const end = value.indexOf("'", 1);
|
|
12489
|
+
return end === -1 ? value.slice(1) : value.slice(1, end);
|
|
12490
|
+
}
|
|
12491
|
+
if (value.startsWith("\"")) return parseDoubleQuotedEnvValue(value);
|
|
12492
|
+
return value.replace(/\s+#.*$/, "").trimEnd();
|
|
12493
|
+
}
|
|
12494
|
+
function parseDoubleQuotedEnvValue(value) {
|
|
12495
|
+
let out = "";
|
|
12496
|
+
let escaped = false;
|
|
12497
|
+
for (let i = 1; i < value.length; i++) {
|
|
12498
|
+
const ch = value[i];
|
|
12499
|
+
if (escaped) {
|
|
12500
|
+
if (ch === "n") out += "\n";
|
|
12501
|
+
else if (ch === "r") out += "\r";
|
|
12502
|
+
else if (ch === "t") out += " ";
|
|
12503
|
+
else out += ch;
|
|
12504
|
+
escaped = false;
|
|
12505
|
+
continue;
|
|
12506
|
+
}
|
|
12507
|
+
if (ch === "\\") {
|
|
12508
|
+
escaped = true;
|
|
12509
|
+
continue;
|
|
12510
|
+
}
|
|
12511
|
+
if (ch === "\"") break;
|
|
12512
|
+
out += ch;
|
|
12513
|
+
}
|
|
12514
|
+
if (escaped) out += "\\";
|
|
12515
|
+
return out;
|
|
12516
|
+
}
|
|
12517
|
+
const SECRET_FLAG_SECURITY_NOTE = "Note: values passed on the command line are visible in shell history (e.g. ~/.bash_history) and to other users via `ps aux` / /proc/[pid]/cmdline. For sensitive values prefer --secret-from-env, --secret-from-file, --secret-from-env-file, or --secret-from-stdin.";
|
|
12518
|
+
const SECRET_SOURCE_FLAGS_DESCRIPTION = "Safe sources: --secret-from-env KEY reads process.env[KEY], --secret-from-file KEY=PATH reads the full UTF-8 file contents, --secret-from-env-file FILE:KEY reads KEY from a dotenv-style file, and --secret-from-stdin KEY reads the value from stdin.";
|
|
12519
|
+
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.";
|
|
10890
12520
|
//#endregion
|
|
10891
12521
|
//#region src/oclif/commands/functions-deploy.ts
|
|
10892
12522
|
async function runDeployWithSecrets(api, params) {
|
|
@@ -10988,22 +12618,23 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
10988
12618
|
\`functions:create-function\` if you need the full flag surface
|
|
10989
12619
|
(raw-body JSON, etc.).
|
|
10990
12620
|
|
|
10991
|
-
Pass
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
push them live.`;
|
|
12621
|
+
Pass secret source flags to seed bindings in the same command. Keys
|
|
12622
|
+
must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
|
|
12623
|
+
underscores; first character is a letter or underscore). With one
|
|
12624
|
+
or more secrets the deploy fans out to multiple API calls:
|
|
12625
|
+
create-function, set-secret per pair, then a final update-function
|
|
12626
|
+
with the same bundle so the running handler picks up the bindings.
|
|
12627
|
+
If a secret write fails after the create step the function exists
|
|
12628
|
+
with whatever secrets succeeded and the redeploy has NOT fired;
|
|
12629
|
+
re-run \`primitive functions set-secret\` for the missing keys, then
|
|
12630
|
+
\`primitive functions redeploy\` to push them live. ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
|
|
11002
12631
|
static summary = "Deploy a new function from a bundled handler file";
|
|
11003
12632
|
static examples = [
|
|
11004
|
-
"<%= config.bin %> functions
|
|
11005
|
-
"<%= config.bin %> functions
|
|
11006
|
-
"<%= config.bin %> functions
|
|
12633
|
+
"<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
|
|
12634
|
+
"<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
|
|
12635
|
+
"<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
|
|
12636
|
+
"<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-env-file .env.local:OWNER_EMAIL",
|
|
12637
|
+
"printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-stdin OPENAI_KEY"
|
|
11007
12638
|
];
|
|
11008
12639
|
static flags = {
|
|
11009
12640
|
"api-key": Flags.string({
|
|
@@ -11028,17 +12659,36 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
11028
12659
|
description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
|
|
11029
12660
|
required: true
|
|
11030
12661
|
}),
|
|
11031
|
-
"source-map-file": Flags.string({ description: "Optional path to a source map for the bundle. Stored
|
|
12662
|
+
"source-map-file": Flags.string({ description: "Optional path to a source map for the bundle. Stored with the deployment attempt and used to symbolicate stack traces in function logs." }),
|
|
11032
12663
|
secret: Flags.string({
|
|
11033
12664
|
description: `Secret KEY=VALUE to seed on the deployed function. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out the deploy to create-function, set-secret per pair, then a final redeploy so the running handler picks up the bindings. ${SECRET_FLAG_SECURITY_NOTE}`,
|
|
11034
12665
|
multiple: true
|
|
11035
12666
|
}),
|
|
12667
|
+
"secret-from-env": Flags.string({
|
|
12668
|
+
description: "Secret KEY to read from the environment and seed on the deployed function. Repeatable. Example: --secret-from-env OPENAI_KEY reads process.env.OPENAI_KEY.",
|
|
12669
|
+
multiple: true
|
|
12670
|
+
}),
|
|
12671
|
+
"secret-from-file": Flags.string({
|
|
12672
|
+
description: "Secret KEY=PATH to read from a UTF-8 file and seed on the deployed function. Repeatable. The full file contents become the value.",
|
|
12673
|
+
multiple: true
|
|
12674
|
+
}),
|
|
12675
|
+
"secret-from-env-file": Flags.string({
|
|
12676
|
+
description: "Secret FILE:KEY to read from a dotenv-style file and seed on the deployed function. Repeatable. Example: --secret-from-env-file .env.local:OPENAI_KEY.",
|
|
12677
|
+
multiple: true
|
|
12678
|
+
}),
|
|
12679
|
+
"secret-from-stdin": Flags.string({ description: "Secret KEY to read from stdin and seed on the deployed function. A single trailing line ending is stripped. Stdin is consumed once, so this flag is not repeatable." }),
|
|
11036
12680
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
11037
12681
|
};
|
|
11038
12682
|
async run() {
|
|
11039
12683
|
const { flags } = await this.parse(FunctionsDeployCommand);
|
|
11040
12684
|
await runWithTiming(flags.time, async () => {
|
|
11041
|
-
const parsedSecrets =
|
|
12685
|
+
const parsedSecrets = resolveSecretFlags({
|
|
12686
|
+
fromEnv: flags["secret-from-env"] ?? [],
|
|
12687
|
+
fromEnvFile: flags["secret-from-env-file"] ?? [],
|
|
12688
|
+
fromFile: flags["secret-from-file"] ?? [],
|
|
12689
|
+
fromStdin: flags["secret-from-stdin"],
|
|
12690
|
+
inline: flags.secret ?? []
|
|
12691
|
+
});
|
|
11042
12692
|
if (parsedSecrets.kind === "error") {
|
|
11043
12693
|
process.stderr.write(`${parsedSecrets.message}\n`);
|
|
11044
12694
|
process.exitCode = 1;
|
|
@@ -11103,10 +12753,10 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
11103
12753
|
const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
|
|
11104
12754
|
const pending = outcome.pendingKeys.length > 0 ? outcome.pendingKeys.join(", ") : "(none)";
|
|
11105
12755
|
const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
|
|
11106
|
-
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions
|
|
12756
|
+
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created, but writing secret ${outcome.failedKey} failed; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The redeploy is NOT yet live. Re-run \`primitive functions set-secret\` for each of [${allMissing}], then \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` to push them live.\n`);
|
|
11107
12757
|
} else if (outcome.stage === "redeploy") {
|
|
11108
12758
|
const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
|
|
11109
|
-
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions
|
|
12759
|
+
process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
|
|
11110
12760
|
}
|
|
11111
12761
|
writeErrorWithHints(outcome.payload);
|
|
11112
12762
|
removeStaleSavedCredentialOnUnauthorized({
|
|
@@ -11122,14 +12772,16 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
|
|
|
11122
12772
|
}
|
|
11123
12773
|
};
|
|
11124
12774
|
//#endregion
|
|
11125
|
-
//#region src/oclif/
|
|
11126
|
-
const
|
|
11127
|
-
const
|
|
12775
|
+
//#region src/oclif/function-templates.ts
|
|
12776
|
+
const DEFAULT_FUNCTION_TEMPLATE_ID = "email-reply";
|
|
12777
|
+
const PRIMITIVE_TEAM_AUTHOR = {
|
|
12778
|
+
id: "primitive-team",
|
|
12779
|
+
name: "Primitive Team",
|
|
12780
|
+
url: "https://primitive.dev"
|
|
12781
|
+
};
|
|
12782
|
+
const SDK_VERSION_RANGE = "^0.29.0";
|
|
12783
|
+
const CLI_VERSION_RANGE = "^0.29.0";
|
|
11128
12784
|
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
11129
|
-
const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
11130
|
-
function isValidFunctionName(name) {
|
|
11131
|
-
return VALID_NAME.test(name);
|
|
11132
|
-
}
|
|
11133
12785
|
function renderHandler() {
|
|
11134
12786
|
return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
|
|
11135
12787
|
//
|
|
@@ -11269,8 +12921,8 @@ function renderPackageJson(name) {
|
|
|
11269
12921
|
type: "module",
|
|
11270
12922
|
scripts: {
|
|
11271
12923
|
build: "node build.mjs",
|
|
11272
|
-
deploy: `npm run build && primitive functions
|
|
11273
|
-
redeploy: "npm run build && primitive functions
|
|
12924
|
+
deploy: `npm run build && primitive functions deploy --name ${name} --file ./dist/handler.js`,
|
|
12925
|
+
redeploy: "npm run build && primitive functions redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js"
|
|
11274
12926
|
},
|
|
11275
12927
|
dependencies: { "@primitivedotdev/sdk": SDK_VERSION_RANGE },
|
|
11276
12928
|
devDependencies: {
|
|
@@ -11341,7 +12993,7 @@ npm run build
|
|
|
11341
12993
|
npm run deploy
|
|
11342
12994
|
\`\`\`
|
|
11343
12995
|
|
|
11344
|
-
The deploy step calls \`primitive functions
|
|
12996
|
+
The deploy step calls \`primitive functions deploy\` (provided by the
|
|
11345
12997
|
\`@primitivedotdev/cli\` package; install with
|
|
11346
12998
|
\`npm install -g @primitivedotdev/cli\` or run via
|
|
11347
12999
|
\`npx @primitivedotdev/cli@latest <command>\`). It requires
|
|
@@ -11350,7 +13002,7 @@ Run \`primitive login\` once to save a key in your CLI config if you
|
|
|
11350
13002
|
prefer that to an env var.
|
|
11351
13003
|
`;
|
|
11352
13004
|
}
|
|
11353
|
-
function
|
|
13005
|
+
function renderEmailReplyTemplateFiles(name) {
|
|
11354
13006
|
return [
|
|
11355
13007
|
{
|
|
11356
13008
|
contents: renderHandler(),
|
|
@@ -11378,9 +13030,79 @@ function scaffoldFiles(name) {
|
|
|
11378
13030
|
}
|
|
11379
13031
|
];
|
|
11380
13032
|
}
|
|
13033
|
+
const FUNCTION_TEMPLATES = [{
|
|
13034
|
+
author: PRIMITIVE_TEAM_AUTHOR,
|
|
13035
|
+
dependencies: ["@primitivedotdev/sdk"],
|
|
13036
|
+
description: "A deployable TypeScript email handler that validates email.received events, skips likely loops, and replies with the Primitive SDK.",
|
|
13037
|
+
devDependencies: [
|
|
13038
|
+
"@primitivedotdev/cli",
|
|
13039
|
+
"esbuild",
|
|
13040
|
+
"typescript"
|
|
13041
|
+
],
|
|
13042
|
+
files: ({ name }) => renderEmailReplyTemplateFiles(name),
|
|
13043
|
+
id: DEFAULT_FUNCTION_TEMPLATE_ID,
|
|
13044
|
+
secrets: [],
|
|
13045
|
+
summary: "Reply to inbound email with the Primitive SDK.",
|
|
13046
|
+
tags: [
|
|
13047
|
+
"email",
|
|
13048
|
+
"reply",
|
|
13049
|
+
"typescript",
|
|
13050
|
+
"worker"
|
|
13051
|
+
],
|
|
13052
|
+
title: "Email Reply"
|
|
13053
|
+
}];
|
|
13054
|
+
function functionTemplateIds(templates) {
|
|
13055
|
+
return templates.map((template) => template.id);
|
|
13056
|
+
}
|
|
13057
|
+
function findFunctionTemplate(templates, id) {
|
|
13058
|
+
return templates.find((template) => template.id === id) ?? null;
|
|
13059
|
+
}
|
|
13060
|
+
function serializeFunctionTemplate(template) {
|
|
13061
|
+
return {
|
|
13062
|
+
author: { ...template.author },
|
|
13063
|
+
dependencies: [...template.dependencies],
|
|
13064
|
+
description: template.description,
|
|
13065
|
+
devDependencies: [...template.devDependencies],
|
|
13066
|
+
id: template.id,
|
|
13067
|
+
secrets: [...template.secrets],
|
|
13068
|
+
summary: template.summary,
|
|
13069
|
+
tags: [...template.tags],
|
|
13070
|
+
title: template.title
|
|
13071
|
+
};
|
|
13072
|
+
}
|
|
13073
|
+
function formatFunctionTemplateList(templates) {
|
|
13074
|
+
const lines = ["Available Primitive Function templates:", ""];
|
|
13075
|
+
for (const template of templates) {
|
|
13076
|
+
lines.push(`${template.id}`);
|
|
13077
|
+
lines.push(` title: ${template.title}`);
|
|
13078
|
+
lines.push(` author: ${template.author.name}`);
|
|
13079
|
+
lines.push(` summary: ${template.summary}`);
|
|
13080
|
+
lines.push(` tags: ${template.tags.length > 0 ? template.tags.join(", ") : "none"}`);
|
|
13081
|
+
lines.push(` secrets: ${template.secrets.length > 0 ? template.secrets.join(", ") : "none"}`);
|
|
13082
|
+
lines.push("");
|
|
13083
|
+
}
|
|
13084
|
+
lines.push("Use `primitive functions init <name> --template <id>`.");
|
|
13085
|
+
return lines.join("\n");
|
|
13086
|
+
}
|
|
13087
|
+
//#endregion
|
|
13088
|
+
//#region src/oclif/commands/functions-init.ts
|
|
13089
|
+
const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
13090
|
+
function isValidFunctionName(name) {
|
|
13091
|
+
return VALID_NAME.test(name);
|
|
13092
|
+
}
|
|
13093
|
+
function unknownTemplateError(templateId) {
|
|
13094
|
+
const available = functionTemplateIds(FUNCTION_TEMPLATES).join(", ");
|
|
13095
|
+
return new Errors.CLIError(`Unknown function template "${templateId}". Available templates: ${available}. Run \`primitive functions templates\` for details.`, { exit: 1 });
|
|
13096
|
+
}
|
|
13097
|
+
function scaffoldFiles(name, templateId = DEFAULT_FUNCTION_TEMPLATE_ID) {
|
|
13098
|
+
const template = findFunctionTemplate(FUNCTION_TEMPLATES, templateId);
|
|
13099
|
+
if (!template) throw unknownTemplateError(templateId);
|
|
13100
|
+
return template.files({ name });
|
|
13101
|
+
}
|
|
11381
13102
|
function writeScaffold(params) {
|
|
11382
13103
|
if (!isValidFunctionName(params.name)) throw new Errors.CLIError(`Invalid function name "${params.name}". Use lowercase letters, digits, hyphens, or underscores (1-63 chars, must start with a letter or digit).`, { exit: 1 });
|
|
11383
|
-
const
|
|
13104
|
+
const templateId = params.templateId ?? "email-reply";
|
|
13105
|
+
const files = scaffoldFiles(params.name, templateId);
|
|
11384
13106
|
const written = [];
|
|
11385
13107
|
try {
|
|
11386
13108
|
mkdirSync(params.outDir, { recursive: false });
|
|
@@ -11411,36 +13133,49 @@ function writeScaffold(params) {
|
|
|
11411
13133
|
return { written };
|
|
11412
13134
|
}
|
|
11413
13135
|
var FunctionsInitCommand = class FunctionsInitCommand extends Command {
|
|
11414
|
-
static description = `Scaffold a new Primitive Function project
|
|
13136
|
+
static description = `Scaffold a new Primitive Function project from a Primitive-owned template.
|
|
11415
13137
|
|
|
11416
13138
|
The scaffolded handler imports \`createPrimitiveClient\` from
|
|
11417
13139
|
\`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
|
|
11418
13140
|
parse the email.received event, send a reply via the SDK, return a
|
|
11419
13141
|
JSON envelope. The build script uses esbuild's JS API and emits
|
|
11420
|
-
./dist/handler.js, ready to hand to \`primitive functions
|
|
13142
|
+
./dist/handler.js, ready to hand to \`primitive functions deploy --file\`.
|
|
11421
13143
|
|
|
11422
13144
|
Refuses to overwrite an existing directory. Use --out-dir to pick a
|
|
11423
|
-
different target path than ./<name
|
|
11424
|
-
|
|
11425
|
-
static
|
|
13145
|
+
different target path than ./<name>/. Run \`primitive functions templates\`
|
|
13146
|
+
to inspect available templates.`;
|
|
13147
|
+
static summary = "Scaffold a new Primitive Function project ready for functions deploy";
|
|
13148
|
+
static examples = [
|
|
13149
|
+
"<%= config.bin %> functions init my-fn",
|
|
13150
|
+
"<%= config.bin %> functions init my-fn --template email-reply",
|
|
13151
|
+
"<%= config.bin %> functions init my-fn --out-dir ./functions/my-fn"
|
|
13152
|
+
];
|
|
11426
13153
|
static args = { name: Args.string({
|
|
11427
13154
|
description: "Function name. Lowercase letters, digits, hyphens, underscores. 1-63 chars. Used as the directory name (when --out-dir is not set) and as the package.json name.",
|
|
11428
13155
|
required: true
|
|
11429
13156
|
}) };
|
|
11430
|
-
static flags = {
|
|
13157
|
+
static flags = {
|
|
13158
|
+
"out-dir": Flags.string({ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist." }),
|
|
13159
|
+
template: Flags.string({
|
|
13160
|
+
default: DEFAULT_FUNCTION_TEMPLATE_ID,
|
|
13161
|
+
description: "Function template id. Run `primitive functions templates` to list templates.",
|
|
13162
|
+
options: functionTemplateIds(FUNCTION_TEMPLATES)
|
|
13163
|
+
})
|
|
13164
|
+
};
|
|
11431
13165
|
async run() {
|
|
11432
13166
|
const { args, flags } = await this.parse(FunctionsInitCommand);
|
|
11433
13167
|
const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
|
|
11434
13168
|
writeScaffold({
|
|
11435
13169
|
name: args.name,
|
|
11436
|
-
outDir
|
|
13170
|
+
outDir,
|
|
13171
|
+
templateId: flags.template
|
|
11437
13172
|
});
|
|
11438
|
-
this.log(`Scaffolded ${outDir}.`);
|
|
13173
|
+
this.log(`Scaffolded ${outDir} from ${flags.template} template.`);
|
|
11439
13174
|
this.log("Next:");
|
|
11440
13175
|
this.log(` cd ${outDir}`);
|
|
11441
13176
|
this.log(" npm install");
|
|
11442
13177
|
this.log(" npm run build");
|
|
11443
|
-
this.log(` primitive functions
|
|
13178
|
+
this.log(` primitive functions deploy --name ${args.name} --file ./dist/handler.js`);
|
|
11444
13179
|
}
|
|
11445
13180
|
};
|
|
11446
13181
|
//#endregion
|
|
@@ -11516,17 +13251,19 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
11516
13251
|
the bindings table fresh on every call, so passing the existing
|
|
11517
13252
|
bundle picks up any secret writes since the last deploy.
|
|
11518
13253
|
|
|
11519
|
-
Pass
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
13254
|
+
Pass secret source flags to write secrets BEFORE the redeploy fires;
|
|
13255
|
+
one update-function call then refreshes every new binding. Keys must
|
|
13256
|
+
match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits, underscores;
|
|
13257
|
+
first character is a letter or underscore). With one or more secrets
|
|
13258
|
+
the redeploy fans out to multiple API calls (set-secret per pair,
|
|
13259
|
+
then update-function). ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
|
|
11525
13260
|
static summary = "Redeploy a function from a bundled handler file";
|
|
11526
13261
|
static examples = [
|
|
11527
|
-
"<%= config.bin %> functions
|
|
11528
|
-
"<%= config.bin %> functions
|
|
11529
|
-
"<%= config.bin %> functions
|
|
13262
|
+
"<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js",
|
|
13263
|
+
"<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
|
|
13264
|
+
"<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
|
|
13265
|
+
"<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-file PRIVATE_KEY=./private-key.pem",
|
|
13266
|
+
"printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-stdin OPENAI_KEY"
|
|
11530
13267
|
];
|
|
11531
13268
|
static flags = {
|
|
11532
13269
|
"api-key": Flags.string({
|
|
@@ -11556,12 +13293,31 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
11556
13293
|
description: `Secret KEY=VALUE to write on the function before the redeploy fires. Repeatable. KEY must match \`^[A-Z_][A-Z0-9_]*$\`; VALUE may contain \`=\` (only the first \`=\` is treated as a delimiter). Each KEY may only appear once per command. Passing one or more --secret flags fans out to set-secret per pair then a single update-function call so the new bindings land in the same redeploy. ${SECRET_FLAG_SECURITY_NOTE}`,
|
|
11557
13294
|
multiple: true
|
|
11558
13295
|
}),
|
|
13296
|
+
"secret-from-env": Flags.string({
|
|
13297
|
+
description: "Secret KEY to read from the environment and write before the redeploy. Repeatable. Example: --secret-from-env OPENAI_KEY reads process.env.OPENAI_KEY.",
|
|
13298
|
+
multiple: true
|
|
13299
|
+
}),
|
|
13300
|
+
"secret-from-file": Flags.string({
|
|
13301
|
+
description: "Secret KEY=PATH to read from a UTF-8 file and write before the redeploy. Repeatable. The full file contents become the value.",
|
|
13302
|
+
multiple: true
|
|
13303
|
+
}),
|
|
13304
|
+
"secret-from-env-file": Flags.string({
|
|
13305
|
+
description: "Secret FILE:KEY to read from a dotenv-style file and write before the redeploy. Repeatable. Example: --secret-from-env-file .env.local:OPENAI_KEY.",
|
|
13306
|
+
multiple: true
|
|
13307
|
+
}),
|
|
13308
|
+
"secret-from-stdin": Flags.string({ description: "Secret KEY to read from stdin and write before the redeploy. A single trailing line ending is stripped. Stdin is consumed once, so this flag is not repeatable." }),
|
|
11559
13309
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
11560
13310
|
};
|
|
11561
13311
|
async run() {
|
|
11562
13312
|
const { flags } = await this.parse(FunctionsRedeployCommand);
|
|
11563
13313
|
await runWithTiming(flags.time, async () => {
|
|
11564
|
-
const parsedSecrets =
|
|
13314
|
+
const parsedSecrets = resolveSecretFlags({
|
|
13315
|
+
fromEnv: flags["secret-from-env"] ?? [],
|
|
13316
|
+
fromEnvFile: flags["secret-from-env-file"] ?? [],
|
|
13317
|
+
fromFile: flags["secret-from-file"] ?? [],
|
|
13318
|
+
fromStdin: flags["secret-from-stdin"],
|
|
13319
|
+
inline: flags.secret ?? []
|
|
13320
|
+
});
|
|
11565
13321
|
if (parsedSecrets.kind === "error") {
|
|
11566
13322
|
process.stderr.write(`${parsedSecrets.message}\n`);
|
|
11567
13323
|
process.exitCode = 1;
|
|
@@ -11617,10 +13373,10 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
|
|
|
11617
13373
|
const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
|
|
11618
13374
|
const pending = outcome.pendingKeys.length > 0 ? outcome.pendingKeys.join(", ") : "(none)";
|
|
11619
13375
|
const allMissing = [outcome.failedKey, ...outcome.pendingKeys].join(", ");
|
|
11620
|
-
process.stderr.write(`Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The new bundle has NOT been deployed. Re-run \`primitive functions
|
|
13376
|
+
process.stderr.write(`Writing secret ${outcome.failedKey} failed before the redeploy; succeeded keys so far: ${succeeded}; keys not yet attempted: ${pending}. The new bundle has NOT been deployed. Re-run \`primitive functions set-secret\` for each of [${allMissing}], then \`primitive functions redeploy --id ${flags.id} --file <bundle>\` to push them live.\n`);
|
|
11621
13377
|
} else if (outcome.stage === "redeploy") {
|
|
11622
13378
|
const succeeded = outcome.succeededKeys.length > 0 ? outcome.succeededKeys.join(", ") : "(none)";
|
|
11623
|
-
process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions
|
|
13379
|
+
process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
|
|
11624
13380
|
}
|
|
11625
13381
|
writeErrorWithHints(outcome.payload);
|
|
11626
13382
|
removeStaleSavedCredentialOnUnauthorized({
|
|
@@ -11702,7 +13458,7 @@ async function runSetSecret(api, params) {
|
|
|
11702
13458
|
};
|
|
11703
13459
|
}
|
|
11704
13460
|
var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command {
|
|
11705
|
-
static description = `Write a function secret and optionally redeploy so the new value lands in the running handler. Agent-grade shortcut for functions
|
|
13461
|
+
static description = `Write a function secret and optionally redeploy so the new value lands in the running handler. Agent-grade shortcut for functions set-function-secret + functions redeploy.
|
|
11706
13462
|
|
|
11707
13463
|
Without --redeploy this is a plain secret upsert: the value is
|
|
11708
13464
|
encrypted at rest but is NOT visible to the running handler until
|
|
@@ -11712,9 +13468,15 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
11712
13468
|
|
|
11713
13469
|
Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
|
|
11714
13470
|
underscores; first character is a letter or underscore). System-
|
|
11715
|
-
managed keys are reserved and rejected
|
|
13471
|
+
managed keys are reserved and rejected. ${SINGLE_SECRET_VALUE_SOURCE_DESCRIPTION}`;
|
|
11716
13472
|
static summary = "Write a function secret (optionally redeploying to push it live)";
|
|
11717
|
-
static examples = [
|
|
13473
|
+
static examples = [
|
|
13474
|
+
"<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123",
|
|
13475
|
+
"<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy",
|
|
13476
|
+
"<%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy",
|
|
13477
|
+
"<%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env-file .env.local --redeploy",
|
|
13478
|
+
"printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --stdin --redeploy"
|
|
13479
|
+
];
|
|
11718
13480
|
static flags = {
|
|
11719
13481
|
"api-key": Flags.string({
|
|
11720
13482
|
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
@@ -11738,11 +13500,12 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
11738
13500
|
description: "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
|
|
11739
13501
|
required: true
|
|
11740
13502
|
}),
|
|
11741
|
-
value: Flags.string({
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
}),
|
|
11745
|
-
|
|
13503
|
+
value: Flags.string({ description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest. Visible in shell history and process argv; prefer a non-argv source for sensitive values." }),
|
|
13504
|
+
"value-from-env": Flags.string({ description: "Environment variable to read as the secret value. Example: --value-from-env OPENAI_KEY reads process.env.OPENAI_KEY." }),
|
|
13505
|
+
"value-file": Flags.string({ description: "UTF-8 file to read as the secret value. The full file contents become the value." }),
|
|
13506
|
+
"value-from-env-file": Flags.string({ description: "Dotenv-style file to read as the secret value. Use FILE to read --key from that file, or FILE:KEY to read a different key." }),
|
|
13507
|
+
stdin: Flags.boolean({ description: "Read the secret value from stdin. A single trailing line ending is stripped. Example: printf '%s' \"$OPENAI_KEY\" | primitive functions set-secret --id <fn-id> --key OPENAI_KEY --stdin" }),
|
|
13508
|
+
redeploy: Flags.boolean({ description: "Also redeploy the function with its current code so the new value lands in the running handler. Without this, the secret is written but not visible to the handler until the next deploy. Note: when --redeploy re-uploads the function's current live code without a sourceMap field, the API preserves the current stored source map. Use `functions redeploy --file <bundle.js> --source-map-file <bundle.js.map>` to replace or restore a map." }),
|
|
11746
13509
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
11747
13510
|
};
|
|
11748
13511
|
async run() {
|
|
@@ -11765,6 +13528,19 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
11765
13528
|
baseUrlOverridden,
|
|
11766
13529
|
configDir: this.config.configDir
|
|
11767
13530
|
};
|
|
13531
|
+
const resolvedValue = resolveSingleSecretValue({
|
|
13532
|
+
key: flags.key,
|
|
13533
|
+
value: flags.value,
|
|
13534
|
+
valueFile: flags["value-file"],
|
|
13535
|
+
valueFromEnv: flags["value-from-env"],
|
|
13536
|
+
valueFromEnvFile: flags["value-from-env-file"],
|
|
13537
|
+
stdin: flags.stdin
|
|
13538
|
+
});
|
|
13539
|
+
if (resolvedValue.kind === "error") {
|
|
13540
|
+
process.stderr.write(`${resolvedValue.message}\n`);
|
|
13541
|
+
process.exitCode = 1;
|
|
13542
|
+
return;
|
|
13543
|
+
}
|
|
11768
13544
|
const outcome = await runSetSecret({
|
|
11769
13545
|
getFunction: (p) => getFunction({
|
|
11770
13546
|
client: apiClient.client,
|
|
@@ -11790,11 +13566,11 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
11790
13566
|
id: flags.id,
|
|
11791
13567
|
key: flags.key,
|
|
11792
13568
|
redeploy: flags.redeploy === true,
|
|
11793
|
-
value:
|
|
13569
|
+
value: resolvedValue.value
|
|
11794
13570
|
});
|
|
11795
13571
|
if (outcome.kind === "error") {
|
|
11796
|
-
if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions
|
|
11797
|
-
else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions
|
|
13572
|
+
if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions redeploy --id <id> --file <bundle>` once you have the bundle.\n");
|
|
13573
|
+
else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
|
|
11798
13574
|
writeErrorWithHints(outcome.payload);
|
|
11799
13575
|
removeStaleSavedCredentialOnUnauthorized({
|
|
11800
13576
|
...authFailureContext,
|
|
@@ -11805,20 +13581,122 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
|
|
|
11805
13581
|
}
|
|
11806
13582
|
this.log(JSON.stringify(outcome.result, null, 2));
|
|
11807
13583
|
});
|
|
11808
|
-
}
|
|
11809
|
-
};
|
|
11810
|
-
//#endregion
|
|
11811
|
-
//#region src/oclif/commands/functions-
|
|
11812
|
-
|
|
11813
|
-
|
|
13584
|
+
}
|
|
13585
|
+
};
|
|
13586
|
+
//#endregion
|
|
13587
|
+
//#region src/oclif/commands/functions-templates.ts
|
|
13588
|
+
var FunctionsTemplatesCommand = class FunctionsTemplatesCommand extends Command {
|
|
13589
|
+
static enableJsonFlag = true;
|
|
13590
|
+
static description = `List Primitive Function templates available to \`primitive functions init\`.
|
|
13591
|
+
|
|
13592
|
+
The default table is optimized for quick terminal discovery. Use
|
|
13593
|
+
--json when an agent or script needs stable metadata for searching,
|
|
13594
|
+
ranking, or choosing a template programmatically.`;
|
|
13595
|
+
static summary = "List available Primitive Function templates";
|
|
13596
|
+
static examples = [
|
|
13597
|
+
"<%= config.bin %> functions templates",
|
|
13598
|
+
"<%= config.bin %> functions templates --json",
|
|
13599
|
+
"<%= config.bin %> functions init my-fn --template email-reply"
|
|
13600
|
+
];
|
|
13601
|
+
static flags = {};
|
|
13602
|
+
async run() {
|
|
13603
|
+
const { flags } = await this.parse(FunctionsTemplatesCommand);
|
|
13604
|
+
if (flags.json) return FUNCTION_TEMPLATES.map(serializeFunctionTemplate);
|
|
13605
|
+
this.log(formatFunctionTemplateList(FUNCTION_TEMPLATES));
|
|
13606
|
+
}
|
|
13607
|
+
};
|
|
13608
|
+
//#endregion
|
|
13609
|
+
//#region src/oclif/commands/functions-test-function.ts
|
|
13610
|
+
const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
|
|
13611
|
+
const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
|
|
13612
|
+
function buildFunctionTestOutcome(params) {
|
|
13613
|
+
const outcome = {
|
|
13614
|
+
elapsed_seconds: params.elapsedSeconds,
|
|
13615
|
+
function_id: params.functionId,
|
|
13616
|
+
inbound_domain: params.invocation.inbound_domain,
|
|
13617
|
+
inbound_id: params.inboundId,
|
|
13618
|
+
inbound_to: params.invocation.to,
|
|
13619
|
+
poll_since: params.invocation.poll_since,
|
|
13620
|
+
test_run_id: params.invocation.test_run_id,
|
|
13621
|
+
test_send_id: params.invocation.send_id,
|
|
13622
|
+
test_subject: params.invocation.subject,
|
|
13623
|
+
trace_url: params.invocation.trace_url,
|
|
13624
|
+
watch_url: params.invocation.watch_url,
|
|
13625
|
+
webhook_attempt_count: params.detail.webhook_attempt_count,
|
|
13626
|
+
webhook_last_error: params.detail.webhook_last_error,
|
|
13627
|
+
webhook_last_status_code: params.detail.webhook_last_status_code,
|
|
13628
|
+
webhook_status: params.detail.webhook_status
|
|
13629
|
+
};
|
|
13630
|
+
if (params.showSends) outcome.sent_emails = params.detail.replies;
|
|
13631
|
+
return outcome;
|
|
13632
|
+
}
|
|
13633
|
+
function stringOrNull(value) {
|
|
13634
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
13635
|
+
}
|
|
13636
|
+
function findMatchingFunctionEndpoints(params) {
|
|
13637
|
+
const matches = [];
|
|
13638
|
+
for (const endpoint of params.endpoints) {
|
|
13639
|
+
if (endpoint.kind !== "function") continue;
|
|
13640
|
+
if (endpoint.enabled === false) continue;
|
|
13641
|
+
if (endpoint.deactivated_at !== null && endpoint.deactivated_at !== void 0) continue;
|
|
13642
|
+
const id = stringOrNull(endpoint.id);
|
|
13643
|
+
if (!id) continue;
|
|
13644
|
+
const domainId = stringOrNull(endpoint.domain_id);
|
|
13645
|
+
if (domainId !== null && (params.inboundDomainId === null || domainId !== params.inboundDomainId)) continue;
|
|
13646
|
+
const functionId = stringOrNull(endpoint.function_id);
|
|
13647
|
+
matches.push({
|
|
13648
|
+
function_id: functionId,
|
|
13649
|
+
id,
|
|
13650
|
+
is_current_function: functionId === params.currentFunctionId,
|
|
13651
|
+
scope: domainId === null ? "catch-all" : "domain"
|
|
13652
|
+
});
|
|
13653
|
+
}
|
|
13654
|
+
return matches;
|
|
13655
|
+
}
|
|
13656
|
+
function formatFunctionEndpointNoiseWarning(params) {
|
|
13657
|
+
if (params.endpoints.filter((endpoint) => !endpoint.is_current_function).length === 0) return null;
|
|
13658
|
+
const lines = [`Warning: ${params.endpoints.length} function endpoints may receive mail for ${params.toAddress}:`];
|
|
13659
|
+
for (const endpoint of params.endpoints) {
|
|
13660
|
+
const scope = endpoint.scope === "catch-all" ? "catch-all" : `scoped to ${params.inboundDomain}`;
|
|
13661
|
+
const current = endpoint.is_current_function ? " (this function)" : "";
|
|
13662
|
+
const target = endpoint.function_id ? ` -> function ${endpoint.function_id}` : "";
|
|
13663
|
+
lines.push(`- endpoint ${endpoint.id}${target}, ${scope}${current}`);
|
|
13664
|
+
}
|
|
13665
|
+
return lines.join("\n");
|
|
13666
|
+
}
|
|
13667
|
+
async function maybeWriteEndpointNoiseWarning(params) {
|
|
13668
|
+
try {
|
|
13669
|
+
const [domainsResult, endpointsResult] = await Promise.all([listDomains({
|
|
13670
|
+
client: params.apiClient.client,
|
|
13671
|
+
responseStyle: "fields"
|
|
13672
|
+
}), listEndpoints({
|
|
13673
|
+
client: params.apiClient.client,
|
|
13674
|
+
responseStyle: "fields"
|
|
13675
|
+
})]);
|
|
13676
|
+
if (endpointsResult.error) return;
|
|
13677
|
+
if (domainsResult.error) return;
|
|
13678
|
+
const inboundDomainId = domainsResult.data?.data?.find((domain) => domain.domain?.toLowerCase() === params.invocation.inbound_domain.toLowerCase())?.id ?? null;
|
|
13679
|
+
const endpointsEnvelope = endpointsResult.data;
|
|
13680
|
+
const warning = formatFunctionEndpointNoiseWarning({
|
|
13681
|
+
endpoints: findMatchingFunctionEndpoints({
|
|
13682
|
+
currentFunctionId: params.currentFunctionId,
|
|
13683
|
+
endpoints: endpointsEnvelope?.data ?? [],
|
|
13684
|
+
inboundDomainId
|
|
13685
|
+
}),
|
|
13686
|
+
inboundDomain: params.invocation.inbound_domain,
|
|
13687
|
+
toAddress: params.invocation.to
|
|
13688
|
+
});
|
|
13689
|
+
if (warning) params.writeStderr(`${warning}\n`);
|
|
13690
|
+
} catch {}
|
|
13691
|
+
}
|
|
11814
13692
|
var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Command {
|
|
11815
13693
|
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.";
|
|
11816
13694
|
static summary = "Trigger a test invocation; with --wait, watch it land";
|
|
11817
13695
|
static examples = [
|
|
11818
|
-
"<%= config.bin %> functions
|
|
11819
|
-
"<%= config.bin %> functions
|
|
11820
|
-
"<%= config.bin %> functions
|
|
11821
|
-
"<%= config.bin %> functions
|
|
13696
|
+
"<%= config.bin %> functions test --id <fn-id>",
|
|
13697
|
+
"<%= config.bin %> functions test --id <fn-id> --local-part summarize",
|
|
13698
|
+
"<%= config.bin %> functions test --id <fn-id> --wait --show-sends",
|
|
13699
|
+
"<%= config.bin %> functions test --id <fn-id> --local-part summarize --wait --timeout 120"
|
|
11822
13700
|
];
|
|
11823
13701
|
static flags = {
|
|
11824
13702
|
"api-key": Flags.string({
|
|
@@ -11894,6 +13772,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
11894
13772
|
this.log(JSON.stringify(invocation, null, 2));
|
|
11895
13773
|
return;
|
|
11896
13774
|
}
|
|
13775
|
+
await maybeWriteEndpointNoiseWarning({
|
|
13776
|
+
apiClient,
|
|
13777
|
+
currentFunctionId: flags.id,
|
|
13778
|
+
invocation,
|
|
13779
|
+
writeStderr: (chunk) => {
|
|
13780
|
+
process.stderr.write(chunk);
|
|
13781
|
+
}
|
|
13782
|
+
});
|
|
11897
13783
|
const startedAt = Date.now();
|
|
11898
13784
|
const timeoutMs = flags.timeout * 1e3;
|
|
11899
13785
|
const pollIntervalMs = flags["poll-interval"] * 1e3;
|
|
@@ -11956,17 +13842,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
11956
13842
|
}
|
|
11957
13843
|
if (!detail) this.error(`Timed out after ${flags.timeout}s waiting for function webhook to fire for inbound ${inboundId}. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
|
|
11958
13844
|
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
11959
|
-
const outcome = {
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
elapsed_seconds: elapsedSeconds
|
|
11968
|
-
};
|
|
11969
|
-
if (shouldShowSends) outcome.sent_emails = detail.replies;
|
|
13845
|
+
const outcome = buildFunctionTestOutcome({
|
|
13846
|
+
detail,
|
|
13847
|
+
elapsedSeconds,
|
|
13848
|
+
functionId: flags.id,
|
|
13849
|
+
inboundId,
|
|
13850
|
+
invocation,
|
|
13851
|
+
showSends: shouldShowSends
|
|
13852
|
+
});
|
|
11970
13853
|
this.log(JSON.stringify(outcome, null, 2));
|
|
11971
13854
|
if (detail.webhook_status === "exhausted") process.exitCode = 1;
|
|
11972
13855
|
});
|
|
@@ -11975,7 +13858,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
|
|
|
11975
13858
|
//#endregion
|
|
11976
13859
|
//#region src/oclif/commands/login.ts
|
|
11977
13860
|
const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
|
|
11978
|
-
function cliError$
|
|
13861
|
+
function cliError$2(message) {
|
|
11979
13862
|
return new Errors.CLIError(message, { exit: 1 });
|
|
11980
13863
|
}
|
|
11981
13864
|
function sleep(ms) {
|
|
@@ -11994,10 +13877,10 @@ function openBrowser(url) {
|
|
|
11994
13877
|
child.on("error", () => void 0);
|
|
11995
13878
|
child.unref();
|
|
11996
13879
|
}
|
|
11997
|
-
function unwrapData$
|
|
13880
|
+
function unwrapData$2(value) {
|
|
11998
13881
|
return value?.data ?? null;
|
|
11999
13882
|
}
|
|
12000
|
-
function retryAfterSeconds(result) {
|
|
13883
|
+
function retryAfterSeconds$1(result) {
|
|
12001
13884
|
const raw = result.response?.headers.get("retry-after");
|
|
12002
13885
|
if (!raw) return null;
|
|
12003
13886
|
const parsed = Number.parseInt(raw, 10);
|
|
@@ -12061,7 +13944,7 @@ var LoginCommand = class LoginCommand extends Command {
|
|
|
12061
13944
|
try {
|
|
12062
13945
|
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
12063
13946
|
} catch (error) {
|
|
12064
|
-
throw cliError$
|
|
13947
|
+
throw cliError$2(error instanceof Error ? error.message : String(error));
|
|
12065
13948
|
}
|
|
12066
13949
|
try {
|
|
12067
13950
|
await this.runWithCredentialLock(flags);
|
|
@@ -12090,8 +13973,8 @@ var LoginCommand = class LoginCommand extends Command {
|
|
|
12090
13973
|
if (existingStatus.status === "removed_stale") process.stderr.write("Continuing with a new Primitive CLI login...\n");
|
|
12091
13974
|
else if (existingStatus.status === "blocked") {
|
|
12092
13975
|
writeErrorWithHints(existingStatus.payload);
|
|
12093
|
-
throw cliError$
|
|
12094
|
-
} else throw cliError$
|
|
13976
|
+
throw cliError$2(existingStatus.message);
|
|
13977
|
+
} else throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before logging in again.`);
|
|
12095
13978
|
}
|
|
12096
13979
|
const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
|
|
12097
13980
|
const started = await startCliLogin({
|
|
@@ -12101,10 +13984,10 @@ var LoginCommand = class LoginCommand extends Command {
|
|
|
12101
13984
|
});
|
|
12102
13985
|
if (started.error) {
|
|
12103
13986
|
writeErrorWithHints(extractErrorPayload(started.error));
|
|
12104
|
-
throw cliError$
|
|
13987
|
+
throw cliError$2("Could not start Primitive CLI login.");
|
|
12105
13988
|
}
|
|
12106
|
-
const start = unwrapData$
|
|
12107
|
-
if (!start) throw cliError$
|
|
13989
|
+
const start = unwrapData$2(started.data);
|
|
13990
|
+
if (!start) throw cliError$2("Primitive API returned an empty CLI login response.");
|
|
12108
13991
|
process.stderr.write(`Your login code is: ${start.user_code}\n`);
|
|
12109
13992
|
if (!flags["no-browser"]) {
|
|
12110
13993
|
openBrowser(start.verification_uri_complete);
|
|
@@ -12124,8 +14007,8 @@ var LoginCommand = class LoginCommand extends Command {
|
|
|
12124
14007
|
responseStyle: "fields"
|
|
12125
14008
|
});
|
|
12126
14009
|
if (polled.data) {
|
|
12127
|
-
const login = unwrapData$
|
|
12128
|
-
if (!login) throw cliError$
|
|
14010
|
+
const login = unwrapData$2(polled.data);
|
|
14011
|
+
if (!login) throw cliError$2("Primitive API returned an empty CLI poll response.");
|
|
12129
14012
|
saveCliCredentials(this.config.configDir, {
|
|
12130
14013
|
api_key: login.api_key,
|
|
12131
14014
|
api_base_url_1: apiBaseUrl1,
|
|
@@ -12147,25 +14030,25 @@ var LoginCommand = class LoginCommand extends Command {
|
|
|
12147
14030
|
continue;
|
|
12148
14031
|
}
|
|
12149
14032
|
if (code === API_ERROR_CODES.slowDown) {
|
|
12150
|
-
interval = Math.min(retryAfterSeconds(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
|
|
14033
|
+
interval = Math.min(retryAfterSeconds$1(polled) ?? interval + 5, MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS);
|
|
12151
14034
|
nextPollDelay = interval;
|
|
12152
14035
|
continue;
|
|
12153
14036
|
}
|
|
12154
|
-
if (code === API_ERROR_CODES.accessDenied) throw cliError$
|
|
12155
|
-
if (code === API_ERROR_CODES.expiredToken) throw cliError$
|
|
12156
|
-
if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$
|
|
14037
|
+
if (code === API_ERROR_CODES.accessDenied) throw cliError$2("Primitive CLI login was denied in the browser.");
|
|
14038
|
+
if (code === API_ERROR_CODES.expiredToken) throw cliError$2("Primitive CLI login expired. Run `primitive login` again.");
|
|
14039
|
+
if (code === API_ERROR_CODES.invalidDeviceCode) throw cliError$2("Primitive CLI login device code is invalid. Run `primitive login` again.");
|
|
12157
14040
|
writeErrorWithHints(payload);
|
|
12158
|
-
throw cliError$
|
|
14041
|
+
throw cliError$2("Primitive CLI login failed while polling for approval.");
|
|
12159
14042
|
}
|
|
12160
|
-
throw cliError$
|
|
14043
|
+
throw cliError$2("Primitive CLI login expired. Run `primitive login` again.");
|
|
12161
14044
|
}
|
|
12162
14045
|
};
|
|
12163
14046
|
//#endregion
|
|
12164
14047
|
//#region src/oclif/commands/logout.ts
|
|
12165
|
-
function cliError(message) {
|
|
14048
|
+
function cliError$1(message) {
|
|
12166
14049
|
return new Errors.CLIError(message, { exit: 1 });
|
|
12167
14050
|
}
|
|
12168
|
-
function unwrapData(value) {
|
|
14051
|
+
function unwrapData$1(value) {
|
|
12169
14052
|
return value?.data ?? null;
|
|
12170
14053
|
}
|
|
12171
14054
|
var LogoutCommand = class LogoutCommand extends Command {
|
|
@@ -12183,7 +14066,7 @@ var LogoutCommand = class LogoutCommand extends Command {
|
|
|
12183
14066
|
try {
|
|
12184
14067
|
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
12185
14068
|
} catch (error) {
|
|
12186
|
-
throw cliError(error instanceof Error ? error.message : String(error));
|
|
14069
|
+
throw cliError$1(error instanceof Error ? error.message : String(error));
|
|
12187
14070
|
}
|
|
12188
14071
|
try {
|
|
12189
14072
|
await this.runWithCredentialLock(flags);
|
|
@@ -12202,7 +14085,7 @@ var LogoutCommand = class LogoutCommand extends Command {
|
|
|
12202
14085
|
process.exitCode = 1;
|
|
12203
14086
|
return;
|
|
12204
14087
|
}
|
|
12205
|
-
if (!credentials) throw cliError("Not logged in. Run `primitive login` to create saved CLI credentials.");
|
|
14088
|
+
if (!credentials) throw cliError$1("Not logged in. Run `primitive login` to create saved CLI credentials.");
|
|
12206
14089
|
const apiBaseUrl1 = flags["api-base-url-1"] ? normalizeApiBaseUrl1(flags["api-base-url-1"]) : credentials.api_base_url_1;
|
|
12207
14090
|
const apiClient = new PrimitiveApiClient({
|
|
12208
14091
|
apiKey: credentials.api_key,
|
|
@@ -12224,15 +14107,101 @@ var LogoutCommand = class LogoutCommand extends Command {
|
|
|
12224
14107
|
return;
|
|
12225
14108
|
}
|
|
12226
14109
|
writeErrorWithHints(payload);
|
|
12227
|
-
throw cliError("Could not revoke the saved Primitive CLI API key.");
|
|
14110
|
+
throw cliError$1("Could not revoke the saved Primitive CLI API key.");
|
|
12228
14111
|
}
|
|
12229
|
-
const logout = unwrapData(result.data);
|
|
14112
|
+
const logout = unwrapData$1(result.data);
|
|
12230
14113
|
deleteCliCredentials(this.config.configDir);
|
|
12231
14114
|
const keyId = logout?.key_id ?? credentials.key_id;
|
|
12232
14115
|
process.stderr.write(`Logged out and revoked API key ${keyId}.\n`);
|
|
12233
14116
|
}
|
|
12234
14117
|
};
|
|
12235
14118
|
//#endregion
|
|
14119
|
+
//#region src/oclif/commands/reply.ts
|
|
14120
|
+
var ReplyCommand = class ReplyCommand extends Command {
|
|
14121
|
+
static description = `Reply to an inbound email.
|
|
14122
|
+
|
|
14123
|
+
The API derives recipients, the Re: subject, and threading headers from the inbound email id. Use \`primitive send --in-reply-to <message-id>\` only when you need to thread against a raw Message-Id instead of an inbound email stored by Primitive.`;
|
|
14124
|
+
static summary = "Reply to an inbound email";
|
|
14125
|
+
static examples = [
|
|
14126
|
+
"<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
|
|
14127
|
+
"<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
|
|
14128
|
+
"<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
|
|
14129
|
+
];
|
|
14130
|
+
static flags = {
|
|
14131
|
+
"api-key": Flags.string({
|
|
14132
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
14133
|
+
env: "PRIMITIVE_API_KEY"
|
|
14134
|
+
}),
|
|
14135
|
+
"api-base-url-1": Flags.string({
|
|
14136
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
14137
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
14138
|
+
hidden: true
|
|
14139
|
+
}),
|
|
14140
|
+
"api-base-url-2": Flags.string({
|
|
14141
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
14142
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
14143
|
+
hidden: true
|
|
14144
|
+
}),
|
|
14145
|
+
id: Flags.string({
|
|
14146
|
+
description: "Inbound email id to reply to.",
|
|
14147
|
+
required: true
|
|
14148
|
+
}),
|
|
14149
|
+
body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
|
|
14150
|
+
html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
|
|
14151
|
+
from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
|
|
14152
|
+
wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
|
|
14153
|
+
"wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
|
|
14154
|
+
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
14155
|
+
};
|
|
14156
|
+
async run() {
|
|
14157
|
+
const { flags } = await this.parse(ReplyCommand);
|
|
14158
|
+
if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
|
|
14159
|
+
await runWithTiming(flags.time, async () => {
|
|
14160
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
|
|
14161
|
+
const auth = resolveCliAuth({
|
|
14162
|
+
apiKey: flags["api-key"],
|
|
14163
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
14164
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
14165
|
+
configDir: this.config.configDir
|
|
14166
|
+
});
|
|
14167
|
+
const apiClient = new PrimitiveApiClient({
|
|
14168
|
+
apiKey: auth.apiKey,
|
|
14169
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
14170
|
+
apiBaseUrl2: auth.apiBaseUrl2
|
|
14171
|
+
});
|
|
14172
|
+
const result = await replyToEmail({
|
|
14173
|
+
body: {
|
|
14174
|
+
...flags.body !== void 0 ? { body_text: flags.body } : {},
|
|
14175
|
+
...flags.html !== void 0 ? { body_html: flags.html } : {},
|
|
14176
|
+
...flags.from !== void 0 ? { from: flags.from } : {},
|
|
14177
|
+
...flags.wait !== void 0 ? { wait: flags.wait } : {},
|
|
14178
|
+
...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
|
|
14179
|
+
},
|
|
14180
|
+
client: apiClient.client,
|
|
14181
|
+
path: { id: flags.id },
|
|
14182
|
+
responseStyle: "fields"
|
|
14183
|
+
});
|
|
14184
|
+
if (result.error) {
|
|
14185
|
+
const errorPayload = extractErrorPayload(result.error);
|
|
14186
|
+
writeErrorWithHints(errorPayload);
|
|
14187
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
14188
|
+
auth,
|
|
14189
|
+
baseUrlOverridden,
|
|
14190
|
+
configDir: this.config.configDir,
|
|
14191
|
+
payload: errorPayload
|
|
14192
|
+
});
|
|
14193
|
+
process.exitCode = 1;
|
|
14194
|
+
return;
|
|
14195
|
+
}
|
|
14196
|
+
const envelope = result.data;
|
|
14197
|
+
writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
|
|
14198
|
+
process.stderr.write(chunk);
|
|
14199
|
+
} });
|
|
14200
|
+
this.log(JSON.stringify(envelope?.data ?? null, null, 2));
|
|
14201
|
+
});
|
|
14202
|
+
}
|
|
14203
|
+
};
|
|
14204
|
+
//#endregion
|
|
12236
14205
|
//#region src/oclif/commands/send.ts
|
|
12237
14206
|
const SUBJECT_MAX_LENGTH = 200;
|
|
12238
14207
|
function deriveSubject(body) {
|
|
@@ -12264,17 +14233,17 @@ async function pickDefaultFromAddress(apiClient, authFailureContext) {
|
|
|
12264
14233
|
throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
|
|
12265
14234
|
}
|
|
12266
14235
|
const first = result.data?.data?.find(isVerifiedDomain);
|
|
12267
|
-
if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains
|
|
14236
|
+
if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains add` and verify it.");
|
|
12268
14237
|
return `agent@${first.domain}`;
|
|
12269
14238
|
}
|
|
12270
14239
|
var SendCommand = class SendCommand extends Command {
|
|
12271
|
-
static description = `Send an outbound email. Agent-grade shortcut for sending
|
|
14240
|
+
static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
|
|
12272
14241
|
|
|
12273
14242
|
--from defaults to agent@<your-first-verified-outbound-domain> when omitted.
|
|
12274
14243
|
--subject defaults to the first line of the body when omitted.
|
|
12275
14244
|
|
|
12276
14245
|
For the full flag set (custom message-id threading on the wire,
|
|
12277
|
-
references arrays, etc.), use \`primitive sending
|
|
14246
|
+
references arrays, etc.), use \`primitive sending send\`.`;
|
|
12278
14247
|
static summary = "Send an email (simplified, agent-friendly)";
|
|
12279
14248
|
static examples = [
|
|
12280
14249
|
"<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
|
|
@@ -12306,7 +14275,7 @@ var SendCommand = class SendCommand extends Command {
|
|
|
12306
14275
|
subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
|
|
12307
14276
|
body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
|
|
12308
14277
|
html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
|
|
12309
|
-
"in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive
|
|
14278
|
+
"in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
|
|
12310
14279
|
wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
|
|
12311
14280
|
"wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
|
|
12312
14281
|
time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
|
|
@@ -12367,6 +14336,394 @@ var SendCommand = class SendCommand extends Command {
|
|
|
12367
14336
|
}
|
|
12368
14337
|
};
|
|
12369
14338
|
//#endregion
|
|
14339
|
+
//#region src/oclif/commands/signup.ts
|
|
14340
|
+
const INVALID_VERIFICATION_CODE = "invalid_verification_code";
|
|
14341
|
+
const CLERK_PASSWORD_REJECTED = "clerk_password_rejected";
|
|
14342
|
+
const EXPIRED_TOKEN = "expired_token";
|
|
14343
|
+
const INVALID_SIGNUP_TOKEN = "invalid_signup_token";
|
|
14344
|
+
const SLOW_DOWN = "slow_down";
|
|
14345
|
+
const PENDING_SIGNUP_FILE = "signup.json";
|
|
14346
|
+
function cliError(message) {
|
|
14347
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
14348
|
+
}
|
|
14349
|
+
function unwrapData(value) {
|
|
14350
|
+
return value?.data ?? null;
|
|
14351
|
+
}
|
|
14352
|
+
function isRecord(value) {
|
|
14353
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14354
|
+
}
|
|
14355
|
+
function pendingSignupFromJson(value) {
|
|
14356
|
+
if (!isRecord(value)) return null;
|
|
14357
|
+
if (typeof value.signup_token !== "string" || typeof value.email !== "string" || typeof value.expires_in !== "number" || typeof value.resend_after !== "number" || typeof value.verification_code_length !== "number" || typeof value.api_base_url_1 !== "string" || typeof value.created_at !== "string" || typeof value.expires_at !== "string") return null;
|
|
14358
|
+
return {
|
|
14359
|
+
api_base_url_1: value.api_base_url_1,
|
|
14360
|
+
created_at: value.created_at,
|
|
14361
|
+
email: value.email,
|
|
14362
|
+
expires_at: value.expires_at,
|
|
14363
|
+
expires_in: value.expires_in,
|
|
14364
|
+
resend_after: value.resend_after,
|
|
14365
|
+
signup_token: value.signup_token,
|
|
14366
|
+
verification_code_length: value.verification_code_length
|
|
14367
|
+
};
|
|
14368
|
+
}
|
|
14369
|
+
function pendingSignupPath(configDir) {
|
|
14370
|
+
return join(configDir, PENDING_SIGNUP_FILE);
|
|
14371
|
+
}
|
|
14372
|
+
function deletePendingCliSignup(configDir) {
|
|
14373
|
+
rmSync(pendingSignupPath(configDir), { force: true });
|
|
14374
|
+
}
|
|
14375
|
+
function pendingSignupFromStart(start, apiBaseUrl1) {
|
|
14376
|
+
return {
|
|
14377
|
+
...start,
|
|
14378
|
+
api_base_url_1: apiBaseUrl1,
|
|
14379
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14380
|
+
expires_at: new Date(Date.now() + start.expires_in * 1e3).toISOString()
|
|
14381
|
+
};
|
|
14382
|
+
}
|
|
14383
|
+
function savePendingCliSignup(configDir, start, apiBaseUrl1) {
|
|
14384
|
+
mkdirSync(configDir, {
|
|
14385
|
+
mode: 448,
|
|
14386
|
+
recursive: true
|
|
14387
|
+
});
|
|
14388
|
+
const pending = pendingSignupFromStart(start, apiBaseUrl1);
|
|
14389
|
+
const path = pendingSignupPath(configDir);
|
|
14390
|
+
const tempPath = join(configDir, `${PENDING_SIGNUP_FILE}.${process$1.pid}.${randomUUID()}.tmp`);
|
|
14391
|
+
try {
|
|
14392
|
+
writeFileSync(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 384 });
|
|
14393
|
+
chmodSync(tempPath, 384);
|
|
14394
|
+
renameSync(tempPath, path);
|
|
14395
|
+
chmodSync(path, 384);
|
|
14396
|
+
return pending;
|
|
14397
|
+
} catch (error) {
|
|
14398
|
+
rmSync(tempPath, { force: true });
|
|
14399
|
+
throw error;
|
|
14400
|
+
}
|
|
14401
|
+
}
|
|
14402
|
+
function loadPendingCliSignup(configDir, apiBaseUrl1) {
|
|
14403
|
+
const path = pendingSignupPath(configDir);
|
|
14404
|
+
let contents;
|
|
14405
|
+
try {
|
|
14406
|
+
contents = readFileSync(path, "utf8");
|
|
14407
|
+
} catch (error) {
|
|
14408
|
+
if (error && typeof error === "object" && error.code === "ENOENT") return null;
|
|
14409
|
+
throw error;
|
|
14410
|
+
}
|
|
14411
|
+
let pending;
|
|
14412
|
+
try {
|
|
14413
|
+
pending = pendingSignupFromJson(JSON.parse(contents));
|
|
14414
|
+
} catch {
|
|
14415
|
+
pending = null;
|
|
14416
|
+
}
|
|
14417
|
+
if (!pending) {
|
|
14418
|
+
deletePendingCliSignup(configDir);
|
|
14419
|
+
return null;
|
|
14420
|
+
}
|
|
14421
|
+
if (pending.api_base_url_1 !== apiBaseUrl1) return null;
|
|
14422
|
+
if (new Date(pending.expires_at).getTime() <= Date.now()) {
|
|
14423
|
+
deletePendingCliSignup(configDir);
|
|
14424
|
+
return null;
|
|
14425
|
+
}
|
|
14426
|
+
return {
|
|
14427
|
+
...pending,
|
|
14428
|
+
expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
|
|
14429
|
+
};
|
|
14430
|
+
}
|
|
14431
|
+
function retryAfterSeconds(result) {
|
|
14432
|
+
const raw = result.response?.headers.get("retry-after");
|
|
14433
|
+
if (!raw) return null;
|
|
14434
|
+
const parsed = Number.parseInt(raw, 10);
|
|
14435
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
14436
|
+
}
|
|
14437
|
+
function normalizeAnswer(value) {
|
|
14438
|
+
return value.trim();
|
|
14439
|
+
}
|
|
14440
|
+
async function promptLine(question) {
|
|
14441
|
+
const rl = createInterface({
|
|
14442
|
+
input: process$1.stdin,
|
|
14443
|
+
output: process$1.stderr
|
|
14444
|
+
});
|
|
14445
|
+
try {
|
|
14446
|
+
return normalizeAnswer(await rl.question(question));
|
|
14447
|
+
} finally {
|
|
14448
|
+
rl.close();
|
|
14449
|
+
}
|
|
14450
|
+
}
|
|
14451
|
+
async function promptHidden(question) {
|
|
14452
|
+
if (!process$1.stdin.isTTY || !process$1.stderr.isTTY || !process$1.stdin.setRawMode) throw cliError("Password input requires an interactive terminal with hidden input support.");
|
|
14453
|
+
return new Promise((resolve, reject) => {
|
|
14454
|
+
const input = process$1.stdin;
|
|
14455
|
+
let value = "";
|
|
14456
|
+
const cleanup = () => {
|
|
14457
|
+
input.setRawMode(false);
|
|
14458
|
+
input.pause();
|
|
14459
|
+
input.off("data", onData);
|
|
14460
|
+
};
|
|
14461
|
+
const finish = () => {
|
|
14462
|
+
cleanup();
|
|
14463
|
+
process$1.stderr.write("\n");
|
|
14464
|
+
resolve(value);
|
|
14465
|
+
};
|
|
14466
|
+
const onData = (chunk) => {
|
|
14467
|
+
const text = chunk.toString("utf8");
|
|
14468
|
+
for (const char of text) {
|
|
14469
|
+
if (char === "") {
|
|
14470
|
+
cleanup();
|
|
14471
|
+
process$1.stderr.write("\n");
|
|
14472
|
+
reject(cliError("Signup cancelled."));
|
|
14473
|
+
return;
|
|
14474
|
+
}
|
|
14475
|
+
if (char === "\r" || char === "\n") {
|
|
14476
|
+
finish();
|
|
14477
|
+
return;
|
|
14478
|
+
}
|
|
14479
|
+
if (char === "\b" || char === "") {
|
|
14480
|
+
value = value.slice(0, -1);
|
|
14481
|
+
continue;
|
|
14482
|
+
}
|
|
14483
|
+
value += char;
|
|
14484
|
+
}
|
|
14485
|
+
};
|
|
14486
|
+
process$1.stderr.write(question);
|
|
14487
|
+
input.setRawMode(true);
|
|
14488
|
+
input.resume();
|
|
14489
|
+
input.on("data", onData);
|
|
14490
|
+
});
|
|
14491
|
+
}
|
|
14492
|
+
function formatSignupSeconds(seconds) {
|
|
14493
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return "soon";
|
|
14494
|
+
if (seconds < 60) return `${Math.ceil(seconds)} seconds`;
|
|
14495
|
+
const minutes = Math.ceil(seconds / 60);
|
|
14496
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
14497
|
+
}
|
|
14498
|
+
function shouldRetrySignupPassword(errorCode) {
|
|
14499
|
+
return errorCode === CLERK_PASSWORD_REJECTED;
|
|
14500
|
+
}
|
|
14501
|
+
function signupErrorMessage(payload) {
|
|
14502
|
+
if (!isRecord(payload)) return null;
|
|
14503
|
+
const message = (isRecord(payload.error) ? payload.error : payload).message;
|
|
14504
|
+
return typeof message === "string" && message.trim() ? message : null;
|
|
14505
|
+
}
|
|
14506
|
+
async function promptRequired(question) {
|
|
14507
|
+
while (true) {
|
|
14508
|
+
const value = await promptLine(question);
|
|
14509
|
+
if (value) return value;
|
|
14510
|
+
process$1.stderr.write("Please enter a value.\n");
|
|
14511
|
+
}
|
|
14512
|
+
}
|
|
14513
|
+
async function promptRequiredPassword(question) {
|
|
14514
|
+
while (true) {
|
|
14515
|
+
const value = await promptHidden(question);
|
|
14516
|
+
if (value) return value;
|
|
14517
|
+
process$1.stderr.write("Please enter a password.\n");
|
|
14518
|
+
}
|
|
14519
|
+
}
|
|
14520
|
+
async function promptNewPassword() {
|
|
14521
|
+
while (true) {
|
|
14522
|
+
const password = await promptRequiredPassword("Password: ");
|
|
14523
|
+
if (password === await promptRequiredPassword("Confirm password: ")) return password;
|
|
14524
|
+
process$1.stderr.write("Passwords did not match. Try again.\n");
|
|
14525
|
+
}
|
|
14526
|
+
}
|
|
14527
|
+
async function confirmTerms() {
|
|
14528
|
+
process$1.stderr.write("By creating an account, you agree to Primitive's Terms of Service and Privacy Policy:\n");
|
|
14529
|
+
process$1.stderr.write(" https://primitive.dev/terms\n");
|
|
14530
|
+
process$1.stderr.write(" https://primitive.dev/privacy\n");
|
|
14531
|
+
const answer = (await promptRequired("Type 'yes' to continue: ")).toLowerCase();
|
|
14532
|
+
if (answer !== "yes" && answer !== "y") throw cliError("You must accept the terms to create an account.");
|
|
14533
|
+
}
|
|
14534
|
+
async function resendVerificationCode(params) {
|
|
14535
|
+
const resent = await (params.deps.resendCliSignupVerification ?? resendCliSignupVerification)({
|
|
14536
|
+
body: { signup_token: params.start.signup_token },
|
|
14537
|
+
client: params.apiClient.client,
|
|
14538
|
+
responseStyle: "fields"
|
|
14539
|
+
});
|
|
14540
|
+
if (resent.data) {
|
|
14541
|
+
const resend = unwrapData(resent.data);
|
|
14542
|
+
const next = resend ? {
|
|
14543
|
+
email: resend.email,
|
|
14544
|
+
expires_in: resend.expires_in,
|
|
14545
|
+
resend_after: resend.resend_after,
|
|
14546
|
+
signup_token: params.start.signup_token,
|
|
14547
|
+
verification_code_length: resend.verification_code_length
|
|
14548
|
+
} : params.start;
|
|
14549
|
+
savePendingCliSignup(params.configDir, next, params.apiBaseUrl1);
|
|
14550
|
+
process$1.stderr.write(`Sent a new ${next.verification_code_length}-digit verification code. It expires in ${formatSignupSeconds(next.expires_in)}.\n`);
|
|
14551
|
+
return next;
|
|
14552
|
+
}
|
|
14553
|
+
const payload = extractErrorPayload(resent.error);
|
|
14554
|
+
if (extractErrorCode(payload) === SLOW_DOWN) {
|
|
14555
|
+
const suffix = ` Wait ${formatSignupSeconds(retryAfterSeconds(resent) ?? params.start.resend_after)} before trying again.`;
|
|
14556
|
+
process$1.stderr.write(`Verification email was sent recently.${suffix}\n`);
|
|
14557
|
+
return params.start;
|
|
14558
|
+
}
|
|
14559
|
+
writeErrorWithHints(payload);
|
|
14560
|
+
throw cliError("Could not resend Primitive CLI signup verification email.");
|
|
14561
|
+
}
|
|
14562
|
+
async function runSignupWithCredentialLock(params) {
|
|
14563
|
+
const { configDir, flags } = params;
|
|
14564
|
+
const deps = params.deps ?? {};
|
|
14565
|
+
const promptRequiredFn = deps.promptRequired ?? promptRequired;
|
|
14566
|
+
const promptNewPasswordFn = deps.promptNewPassword ?? promptNewPassword;
|
|
14567
|
+
const confirmTermsFn = deps.confirmTerms ?? confirmTerms;
|
|
14568
|
+
const startFn = deps.startCliSignup ?? startCliSignup;
|
|
14569
|
+
const verifyFn = deps.verifyCliSignup ?? verifyCliSignup;
|
|
14570
|
+
const checkExistingLoginFn = deps.checkExistingLogin ?? checkExistingLogin;
|
|
14571
|
+
const apiBaseUrl1 = normalizeApiBaseUrl1(flags["api-base-url-1"]);
|
|
14572
|
+
let existing;
|
|
14573
|
+
try {
|
|
14574
|
+
existing = loadCliCredentials(configDir);
|
|
14575
|
+
} catch (error) {
|
|
14576
|
+
if (!flags.force) throw error;
|
|
14577
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
14578
|
+
process$1.stderr.write(`Replacing unreadable Primitive CLI credentials because --force was set: ${detail}\n`);
|
|
14579
|
+
existing = null;
|
|
14580
|
+
}
|
|
14581
|
+
if (existing && flags.force) process$1.stderr.write("Replacing saved Primitive CLI credentials after signup because --force was set.\n");
|
|
14582
|
+
else if (existing) {
|
|
14583
|
+
const existingStatus = await checkExistingLoginFn({
|
|
14584
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
14585
|
+
configDir,
|
|
14586
|
+
credentials: existing
|
|
14587
|
+
});
|
|
14588
|
+
if (existingStatus.status === "removed_stale") process$1.stderr.write("Continuing with Primitive CLI signup...\n");
|
|
14589
|
+
else if (existingStatus.status === "blocked") {
|
|
14590
|
+
writeErrorWithHints(existingStatus.payload);
|
|
14591
|
+
throw cliError(existingStatus.message);
|
|
14592
|
+
} else throw cliError(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before creating a new account.`);
|
|
14593
|
+
}
|
|
14594
|
+
if (flags.force) deletePendingCliSignup(configDir);
|
|
14595
|
+
const apiClient = new PrimitiveApiClient({ apiBaseUrl1 });
|
|
14596
|
+
let start = flags.force ? null : loadPendingCliSignup(configDir, apiBaseUrl1);
|
|
14597
|
+
const resumed = Boolean(start);
|
|
14598
|
+
if (start) process$1.stderr.write(`Continuing pending Primitive CLI signup for ${start.email}.\n`);
|
|
14599
|
+
else {
|
|
14600
|
+
const signupCode = await promptRequiredFn("Signup code: ");
|
|
14601
|
+
await confirmTermsFn();
|
|
14602
|
+
const email = await promptRequiredFn("Email: ");
|
|
14603
|
+
const started = await startFn({
|
|
14604
|
+
body: {
|
|
14605
|
+
device_name: flags["device-name"] ?? hostname(),
|
|
14606
|
+
email,
|
|
14607
|
+
signup_code: signupCode,
|
|
14608
|
+
terms_accepted: true
|
|
14609
|
+
},
|
|
14610
|
+
client: apiClient.client,
|
|
14611
|
+
responseStyle: "fields"
|
|
14612
|
+
});
|
|
14613
|
+
if (started.error) {
|
|
14614
|
+
writeErrorWithHints(extractErrorPayload(started.error));
|
|
14615
|
+
throw cliError("Could not start Primitive CLI signup.");
|
|
14616
|
+
}
|
|
14617
|
+
const startResult = unwrapData(started.data);
|
|
14618
|
+
if (!startResult) throw cliError("Primitive API returned an empty CLI signup response.");
|
|
14619
|
+
start = savePendingCliSignup(configDir, startResult, apiBaseUrl1);
|
|
14620
|
+
}
|
|
14621
|
+
if (resumed) process$1.stderr.write(`Check your email for the ${start.verification_code_length}-digit verification code sent to ${start.email}.\n`);
|
|
14622
|
+
else process$1.stderr.write(`Sent a ${start.verification_code_length}-digit verification code to ${start.email}.\n`);
|
|
14623
|
+
process$1.stderr.write(`The code expires in ${formatSignupSeconds(start.expires_in)}.\n`);
|
|
14624
|
+
process$1.stderr.write(`Enter the code from the email, or type \`resend\` to send a new code after ${formatSignupSeconds(start.resend_after)}.\n`);
|
|
14625
|
+
while (true) {
|
|
14626
|
+
const verificationCode = await promptRequiredFn(`Verification code (${start.verification_code_length} digits): `);
|
|
14627
|
+
if (verificationCode.toLowerCase() === "resend") {
|
|
14628
|
+
start = await resendVerificationCode({
|
|
14629
|
+
apiBaseUrl1,
|
|
14630
|
+
apiClient,
|
|
14631
|
+
configDir,
|
|
14632
|
+
deps,
|
|
14633
|
+
start
|
|
14634
|
+
});
|
|
14635
|
+
continue;
|
|
14636
|
+
}
|
|
14637
|
+
let password = await promptNewPasswordFn();
|
|
14638
|
+
while (true) {
|
|
14639
|
+
const verified = await verifyFn({
|
|
14640
|
+
body: {
|
|
14641
|
+
password,
|
|
14642
|
+
signup_token: start.signup_token,
|
|
14643
|
+
verification_code: verificationCode
|
|
14644
|
+
},
|
|
14645
|
+
client: apiClient.client,
|
|
14646
|
+
responseStyle: "fields"
|
|
14647
|
+
});
|
|
14648
|
+
if (verified.data) {
|
|
14649
|
+
const signup = unwrapData(verified.data);
|
|
14650
|
+
if (!signup) throw cliError("Primitive API returned an empty CLI signup verification response.");
|
|
14651
|
+
saveCliCredentials(configDir, {
|
|
14652
|
+
api_key: signup.api_key,
|
|
14653
|
+
api_base_url_1: apiBaseUrl1,
|
|
14654
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14655
|
+
key_id: signup.key_id,
|
|
14656
|
+
key_prefix: signup.key_prefix,
|
|
14657
|
+
org_id: signup.org_id,
|
|
14658
|
+
org_name: signup.org_name
|
|
14659
|
+
});
|
|
14660
|
+
deletePendingCliSignup(configDir);
|
|
14661
|
+
const org = signup.org_name ? ` (${signup.org_name})` : "";
|
|
14662
|
+
process$1.stderr.write(`Created account and logged in to org ${signup.org_id}${org}.\n`);
|
|
14663
|
+
process$1.stderr.write(`Saved credentials to ${credentialsPath(configDir)}.\n`);
|
|
14664
|
+
return;
|
|
14665
|
+
}
|
|
14666
|
+
const payload = extractErrorPayload(verified.error);
|
|
14667
|
+
const code = extractErrorCode(payload);
|
|
14668
|
+
if (code === INVALID_VERIFICATION_CODE) {
|
|
14669
|
+
process$1.stderr.write("Invalid verification code. Try again or type `resend`.\n");
|
|
14670
|
+
break;
|
|
14671
|
+
}
|
|
14672
|
+
if (shouldRetrySignupPassword(code)) {
|
|
14673
|
+
const message = signupErrorMessage(payload);
|
|
14674
|
+
if (message) process$1.stderr.write(`Password rejected: ${message}\n`);
|
|
14675
|
+
process$1.stderr.write("Choose a different password and try again.\n");
|
|
14676
|
+
password = await promptNewPasswordFn();
|
|
14677
|
+
continue;
|
|
14678
|
+
}
|
|
14679
|
+
if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingCliSignup(configDir);
|
|
14680
|
+
writeErrorWithHints(payload);
|
|
14681
|
+
throw cliError("Primitive CLI signup failed while verifying the account.");
|
|
14682
|
+
}
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14685
|
+
var SignupCommand = class SignupCommand extends Command {
|
|
14686
|
+
static description = "Create a Primitive account from the terminal, verify your email, and save an org-scoped CLI API key locally.";
|
|
14687
|
+
static summary = "Create an account and log in";
|
|
14688
|
+
static examples = [
|
|
14689
|
+
"<%= config.bin %> signup",
|
|
14690
|
+
"<%= config.bin %> signup --device-name work-laptop",
|
|
14691
|
+
"<%= config.bin %> signup --force"
|
|
14692
|
+
];
|
|
14693
|
+
static flags = {
|
|
14694
|
+
"api-base-url-1": Flags.string({
|
|
14695
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
14696
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
14697
|
+
hidden: true
|
|
14698
|
+
}),
|
|
14699
|
+
"device-name": Flags.string({ description: "Device name used for the created CLI API key" }),
|
|
14700
|
+
force: Flags.boolean({
|
|
14701
|
+
char: "f",
|
|
14702
|
+
description: "Replace saved credentials without first verifying the existing login"
|
|
14703
|
+
})
|
|
14704
|
+
};
|
|
14705
|
+
async run() {
|
|
14706
|
+
const { flags } = await this.parse(SignupCommand);
|
|
14707
|
+
let releaseCredentialsLock;
|
|
14708
|
+
try {
|
|
14709
|
+
releaseCredentialsLock = acquireCliCredentialsLock(this.config.configDir);
|
|
14710
|
+
} catch (error) {
|
|
14711
|
+
throw cliError(error instanceof Error ? error.message : String(error));
|
|
14712
|
+
}
|
|
14713
|
+
try {
|
|
14714
|
+
await this.runWithCredentialLock(flags);
|
|
14715
|
+
} finally {
|
|
14716
|
+
releaseCredentialsLock();
|
|
14717
|
+
}
|
|
14718
|
+
}
|
|
14719
|
+
async runWithCredentialLock(flags) {
|
|
14720
|
+
await runSignupWithCredentialLock({
|
|
14721
|
+
configDir: this.config.configDir,
|
|
14722
|
+
flags
|
|
14723
|
+
});
|
|
14724
|
+
}
|
|
14725
|
+
};
|
|
14726
|
+
//#endregion
|
|
12370
14727
|
//#region src/oclif/commands/whoami.ts
|
|
12371
14728
|
var WhoamiCommand = class WhoamiCommand extends Command {
|
|
12372
14729
|
static description = `Print the account currently authenticated by the API key. Useful as a credentials smoke test: confirms the key is live and shows which account it belongs to.`;
|
|
@@ -12509,7 +14866,7 @@ var ListOperationsCommand = class extends Command {
|
|
|
12509
14866
|
}
|
|
12510
14867
|
};
|
|
12511
14868
|
function lookupOperation(id) {
|
|
12512
|
-
const trimmed = id.trim();
|
|
14869
|
+
const trimmed = resolveOperationAlias(id.trim());
|
|
12513
14870
|
const sep = trimmed.indexOf(":");
|
|
12514
14871
|
const tag = sep === -1 ? "" : trimmed.slice(0, sep);
|
|
12515
14872
|
const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
|
|
@@ -12525,7 +14882,7 @@ function lookupOperation(id) {
|
|
|
12525
14882
|
}
|
|
12526
14883
|
var DescribeCommand = class DescribeCommand extends Command {
|
|
12527
14884
|
static args = { command: Args.string({
|
|
12528
|
-
description: "Command id to describe,
|
|
14885
|
+
description: "Command id to describe, e.g. `emails:list` or `emails:get-email`. Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate generated operation ids.",
|
|
12529
14886
|
required: true
|
|
12530
14887
|
}) };
|
|
12531
14888
|
static description = `Print the full operation manifest entry for a single API command, including the path, request schema, response schema, and per-field descriptions sourced from the OpenAPI spec.
|
|
@@ -12533,15 +14890,15 @@ var DescribeCommand = class DescribeCommand extends Command {
|
|
|
12533
14890
|
The manifest entry's \`responseSchema\` carries the inlined JSON Schema for the operation's 200/201 \`data\` envelope contents (\`$ref\`s resolved). Use it to look up what specific response fields mean. Examples:
|
|
12534
14891
|
|
|
12535
14892
|
# Which of EmailDetail's sender-shaped fields is canonical?
|
|
12536
|
-
primitive describe emails:get
|
|
12537
|
-
primitive describe emails:get
|
|
14893
|
+
primitive describe emails:get | jq '.responseSchema.properties | keys'
|
|
14894
|
+
primitive describe emails:get | jq -r '.responseSchema.properties.from_email.description'
|
|
12538
14895
|
|
|
12539
14896
|
# What does each value of SentEmailStatus mean?
|
|
12540
|
-
primitive describe
|
|
14897
|
+
primitive describe sent:get | jq -r '.responseSchema.properties.status.description'
|
|
12541
14898
|
|
|
12542
14899
|
\`requestSchema\` is the same shape for the request body when one exists. For a single field across many operations at once, use \`primitive list-operations | jq\` instead.`;
|
|
12543
14900
|
static summary = "Describe a single API operation in detail";
|
|
12544
|
-
static examples = ["<%= config.bin %> describe emails:get
|
|
14901
|
+
static examples = ["<%= config.bin %> describe emails:get", "<%= config.bin %> describe sent:get"];
|
|
12545
14902
|
async run() {
|
|
12546
14903
|
const { args } = await this.parse(DescribeCommand);
|
|
12547
14904
|
const { match, candidates } = lookupOperation(args.command);
|
|
@@ -12577,13 +14934,66 @@ var CompletionCommand = class CompletionCommand extends Command {
|
|
|
12577
14934
|
function commandId(operation) {
|
|
12578
14935
|
return `${operation.tagCommand}:${operation.command}`;
|
|
12579
14936
|
}
|
|
14937
|
+
const CANONICAL_OPERATION_ALIASES = {
|
|
14938
|
+
"account:show": "account:get-account",
|
|
14939
|
+
"account:storage": "account:get-storage-stats",
|
|
14940
|
+
"account:webhook-secret": "account:get-webhook-secret",
|
|
14941
|
+
"deliveries:list": "webhook-deliveries:list-deliveries",
|
|
14942
|
+
"deliveries:replay": "webhook-deliveries:replay-delivery",
|
|
14943
|
+
"domains:add": "domains:add-domain",
|
|
14944
|
+
"domains:delete": "domains:delete-domain",
|
|
14945
|
+
"domains:list": "domains:list-domains",
|
|
14946
|
+
"domains:update": "domains:update-domain",
|
|
14947
|
+
"domains:verify": "domains:verify-domain",
|
|
14948
|
+
"emails:delete": "emails:delete-email",
|
|
14949
|
+
"emails:discard-content": "emails:discard-email-content",
|
|
14950
|
+
"emails:download-raw": "emails:download-raw-email",
|
|
14951
|
+
"emails:get": "emails:get-email",
|
|
14952
|
+
"emails:list": "emails:list-emails",
|
|
14953
|
+
"emails:replay-webhooks": "emails:replay-email-webhooks",
|
|
14954
|
+
"emails:search": "emails:search-emails",
|
|
14955
|
+
"endpoints:create": "endpoints:create-endpoint",
|
|
14956
|
+
"endpoints:delete": "endpoints:delete-endpoint",
|
|
14957
|
+
"endpoints:list": "endpoints:list-endpoints",
|
|
14958
|
+
"endpoints:test": "endpoints:test-endpoint",
|
|
14959
|
+
"endpoints:update": "endpoints:update-endpoint",
|
|
14960
|
+
"filters:create": "filters:create-filter",
|
|
14961
|
+
"filters:delete": "filters:delete-filter",
|
|
14962
|
+
"filters:list": "filters:list-filters",
|
|
14963
|
+
"filters:update": "filters:update-filter",
|
|
14964
|
+
"functions:delete": "functions:delete-function",
|
|
14965
|
+
"functions:delete-secret": "functions:delete-function-secret",
|
|
14966
|
+
"functions:get": "functions:get-function",
|
|
14967
|
+
"functions:list": "functions:list-functions",
|
|
14968
|
+
"functions:list-secrets": "functions:list-function-secrets",
|
|
14969
|
+
"functions:logs": "functions:list-function-logs",
|
|
14970
|
+
"sending:get": "sending:get-sent-email",
|
|
14971
|
+
"sending:list": "sending:list-sent-emails",
|
|
14972
|
+
"sending:permissions": "sending:get-send-permissions",
|
|
14973
|
+
"sending:reply": "sending:reply-to-email",
|
|
14974
|
+
"sending:send": "sending:send-email",
|
|
14975
|
+
"sent:get": "sending:get-sent-email",
|
|
14976
|
+
"sent:list": "sending:list-sent-emails",
|
|
14977
|
+
"webhook-deliveries:list": "webhook-deliveries:list-deliveries",
|
|
14978
|
+
"webhook-deliveries:replay": "webhook-deliveries:replay-delivery"
|
|
14979
|
+
};
|
|
14980
|
+
const DESCRIBE_OPERATION_ALIASES = {
|
|
14981
|
+
...CANONICAL_OPERATION_ALIASES,
|
|
14982
|
+
reply: "sending:reply-to-email"
|
|
14983
|
+
};
|
|
14984
|
+
function resolveOperationAlias(id) {
|
|
14985
|
+
return DESCRIBE_OPERATION_ALIASES[id] ?? id;
|
|
14986
|
+
}
|
|
12580
14987
|
const OVERRIDDEN_OPERATION_IDS = new Set(["functions:test-function"]);
|
|
14988
|
+
const generatedCommands = Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation))).map((operation) => [commandId(operation), createOperationCommand(operation)]));
|
|
12581
14989
|
const COMMANDS = {
|
|
12582
14990
|
completion: CompletionCommand,
|
|
12583
14991
|
"list-operations": ListOperationsCommand,
|
|
12584
14992
|
describe: DescribeCommand,
|
|
12585
14993
|
send: SendCommand,
|
|
14994
|
+
reply: ReplyCommand,
|
|
12586
14995
|
login: LoginCommand,
|
|
14996
|
+
signup: SignupCommand,
|
|
12587
14997
|
logout: LogoutCommand,
|
|
12588
14998
|
whoami: WhoamiCommand,
|
|
12589
14999
|
doctor: DoctorCommand,
|
|
@@ -12591,11 +15001,18 @@ const COMMANDS = {
|
|
|
12591
15001
|
"emails:watch": EmailsWatchCommand,
|
|
12592
15002
|
"emails:wait": EmailsWaitCommand,
|
|
12593
15003
|
"functions:init": FunctionsInitCommand,
|
|
15004
|
+
"functions:templates": FunctionsTemplatesCommand,
|
|
12594
15005
|
"functions:deploy": FunctionsDeployCommand,
|
|
12595
15006
|
"functions:redeploy": FunctionsRedeployCommand,
|
|
12596
15007
|
"functions:set-secret": FunctionsSetSecretCommand,
|
|
15008
|
+
"functions:test": FunctionsTestFunctionCommand,
|
|
12597
15009
|
"functions:test-function": FunctionsTestFunctionCommand,
|
|
12598
|
-
...Object.fromEntries(
|
|
15010
|
+
...Object.fromEntries(Object.entries(CANONICAL_OPERATION_ALIASES).map(([alias, target]) => {
|
|
15011
|
+
const command = generatedCommands[target];
|
|
15012
|
+
if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
|
|
15013
|
+
return [alias, command];
|
|
15014
|
+
})),
|
|
15015
|
+
...generatedCommands
|
|
12599
15016
|
};
|
|
12600
15017
|
//#endregion
|
|
12601
|
-
export { COMMANDS, lookupOperation };
|
|
15018
|
+
export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };
|