@primitivedotdev/cli 0.28.0 → 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.
@@ -649,6 +649,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
649
649
  getAccount: () => getAccount,
650
650
  getEmail: () => getEmail,
651
651
  getFunction: () => getFunction,
652
+ getFunctionTestRunTrace: () => getFunctionTestRunTrace,
652
653
  getSendPermissions: () => getSendPermissions,
653
654
  getSentEmail: () => getSentEmail,
654
655
  getStorageStats: () => getStorageStats,
@@ -1635,6 +1636,24 @@ const testFunction = (options) => (options.client ?? client).post({
1635
1636
  }
1636
1637
  });
1637
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
+ /**
1638
1657
  * List a function's secrets
1639
1658
  *
1640
1659
  * Returns metadata for every secret bound to the function, with
@@ -3294,6 +3313,37 @@ const openapiDocument = {
3294
3313
  }
3295
3314
  }
3296
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
+ },
3297
3347
  "/functions/{id}/secrets": {
3298
3348
  "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3299
3349
  "get": {
@@ -5594,8 +5644,13 @@ const openapiDocument = {
5594
5644
  },
5595
5645
  "TestInvocationResult": {
5596
5646
  "type": "object",
5597
- "description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; the actual invocation lands on the function's\ninvocations list a few seconds later as the inbound mail\ntraverses the MX path.\n",
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",
5598
5648
  "properties": {
5649
+ "test_run_id": {
5650
+ "type": "string",
5651
+ "format": "uuid",
5652
+ "description": "Durable test run id used to fetch the run trace."
5653
+ },
5599
5654
  "inbound_domain": {
5600
5655
  "type": "string",
5601
5656
  "description": "Verified inbound domain the test email was sent to."
@@ -5625,16 +5680,339 @@ const openapiDocument = {
5625
5680
  "type": "string",
5626
5681
  "format": "uri",
5627
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."
5628
5687
  }
5629
5688
  },
5630
5689
  "required": [
5690
+ "test_run_id",
5631
5691
  "inbound_domain",
5632
5692
  "to",
5633
5693
  "from",
5634
5694
  "send_id",
5635
5695
  "subject",
5636
5696
  "poll_since",
5637
- "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"
5638
6016
  ]
5639
6017
  },
5640
6018
  "FunctionLogRow": {
@@ -8266,23 +8644,456 @@ const operationManifest = [
8266
8644
  "type": "string",
8267
8645
  "format": "date-time"
8268
8646
  },
8269
- "updated_at": {
8270
- "type": "string",
8271
- "format": "date-time"
8647
+ "updated_at": {
8648
+ "type": "string",
8649
+ "format": "date-time"
8650
+ }
8651
+ },
8652
+ "required": [
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
+ }
8272
9082
  }
8273
9083
  },
8274
9084
  "required": [
8275
- "id",
8276
- "name",
8277
- "code",
8278
- "deploy_status",
8279
- "gateway_url",
8280
- "created_at",
8281
- "updated_at"
9085
+ "state",
9086
+ "test_run",
9087
+ "test_send",
9088
+ "inbound_email",
9089
+ "deliveries",
9090
+ "outbound_requests",
9091
+ "logs",
9092
+ "replies"
8282
9093
  ]
8283
9094
  },
8284
- "sdkName": "getFunction",
8285
- "summary": "Get a function",
9095
+ "sdkName": "getFunctionTestRunTrace",
9096
+ "summary": "Get a function test run trace",
8286
9097
  "tag": "Functions",
8287
9098
  "tagCommand": "functions"
8288
9099
  },
@@ -8602,8 +9413,13 @@ const operationManifest = [
8602
9413
  },
8603
9414
  "responseSchema": {
8604
9415
  "type": "object",
8605
- "description": "Metadata returned by POST /functions/{id}/test. The send is\nqueued; the actual invocation lands on the function's\ninvocations list a few seconds later as the inbound mail\ntraverses the MX path.\n",
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",
8606
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
+ },
8607
9423
  "inbound_domain": {
8608
9424
  "type": "string",
8609
9425
  "description": "Verified inbound domain the test email was sent to."
@@ -8633,16 +9449,22 @@ const operationManifest = [
8633
9449
  "type": "string",
8634
9450
  "format": "uri",
8635
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."
8636
9456
  }
8637
9457
  },
8638
9458
  "required": [
9459
+ "test_run_id",
8639
9460
  "inbound_domain",
8640
9461
  "to",
8641
9462
  "from",
8642
9463
  "send_id",
8643
9464
  "subject",
8644
9465
  "poll_since",
8645
- "watch_url"
9466
+ "watch_url",
9467
+ "trace_url"
8646
9468
  ]
8647
9469
  },
8648
9470
  "sdkName": "testFunction",
@@ -11382,41 +12204,319 @@ function emitRawSendMailFetchWarning(bundleText, write) {
11382
12204
  //#endregion
11383
12205
  //#region src/oclif/secret-flags.ts
11384
12206
  const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
11385
- function parseSecretFlags(raw) {
12207
+ function resolveSecretFlags(input) {
11386
12208
  const secrets = [];
11387
12209
  const seenKeys = /* @__PURE__ */ new Set();
11388
- for (const entry of raw) {
11389
- const eq = entry.indexOf("=");
11390
- if (eq === -1) return {
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 {
11391
12241
  kind: "error",
11392
- message: `--secret expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`
12242
+ message: `--secret-from-env ${key} could not read ${key}: environment variable is not set.`
11393
12243
  };
11394
- const key = entry.slice(0, eq);
11395
- const value = entry.slice(eq + 1);
11396
- if (key.length === 0) return {
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 {
11397
12270
  kind: "error",
11398
- message: `--secret is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`
12271
+ message: `--secret-from-env-file ${entry} could not read ${parsed.key}: key is not present in ${parsed.path}.`
11399
12272
  };
11400
- if (!SECRET_KEY_RE.test(key)) return {
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 {
11401
12314
  kind: "error",
11402
- message: `--secret KEY ${JSON.stringify(key)} does not match ${SECRET_KEY_RE.source} (uppercase letters, digits, underscores; first character is a letter or underscore).`
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
11403
12320
  };
11404
- if (seenKeys.has(key)) return {
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 {
11405
12330
  kind: "error",
11406
- message: `--secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`
12331
+ message: `--value-from-env-file ${input.valueFromEnvFile} could not read ${parsed.key}: key is not present in ${parsed.path}.`
11407
12332
  };
11408
- seenKeys.add(key);
11409
- secrets.push({
11410
- key,
12333
+ return {
12334
+ kind: "ok",
11411
12335
  value
11412
- });
12336
+ };
11413
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
+ };
11414
12363
  return {
11415
12364
  kind: "ok",
11416
- secrets
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
11417
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
12470
+ };
12471
+ }
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;
11418
12516
  }
11419
- 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 KEY=\"$VAR\"` where `$VAR` is set out-of-band (read -s, a secrets manager, etc.).";
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.";
11420
12520
  //#endregion
11421
12521
  //#region src/oclif/commands/functions-deploy.ts
11422
12522
  async function runDeployWithSecrets(api, params) {
@@ -11518,22 +12618,23 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11518
12618
  \`functions:create-function\` if you need the full flag surface
11519
12619
  (raw-body JSON, etc.).
11520
12620
 
11521
- Pass --secret KEY=VALUE (repeatable) to seed secret bindings in the
11522
- same command. Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase
11523
- letters, digits, underscores; first character is a letter or
11524
- underscore). With one or more --secret flags the deploy fans out to
11525
- multiple API calls: create-function, set-secret per pair, then a
11526
- final update-function with the same bundle so the running handler
11527
- picks up the bindings. If a secret write fails after the create
11528
- step the function exists with whatever secrets succeeded and the
11529
- redeploy has NOT fired; re-run \`primitive functions set-secret\`
11530
- for the missing keys, then \`primitive functions redeploy\` to
11531
- 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}`;
11532
12631
  static summary = "Deploy a new function from a bundled handler file";
11533
12632
  static examples = [
11534
12633
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
11535
12634
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
11536
- "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
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"
11537
12638
  ];
11538
12639
  static flags = {
11539
12640
  "api-key": Flags.string({
@@ -11563,12 +12664,31 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11563
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}`,
11564
12665
  multiple: true
11565
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." }),
11566
12680
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11567
12681
  };
11568
12682
  async run() {
11569
12683
  const { flags } = await this.parse(FunctionsDeployCommand);
11570
12684
  await runWithTiming(flags.time, async () => {
11571
- const parsedSecrets = parseSecretFlags(flags.secret ?? []);
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
+ });
11572
12692
  if (parsedSecrets.kind === "error") {
11573
12693
  process.stderr.write(`${parsedSecrets.message}\n`);
11574
12694
  process.exitCode = 1;
@@ -11652,14 +12772,16 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11652
12772
  }
11653
12773
  };
11654
12774
  //#endregion
11655
- //#region src/oclif/commands/functions-init.ts
11656
- const SDK_VERSION_RANGE = "^0.28.0";
11657
- const CLI_VERSION_RANGE = "^0.28.0";
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";
11658
12784
  const ESBUILD_VERSION_RANGE = "^0.27.0";
11659
- const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
11660
- function isValidFunctionName(name) {
11661
- return VALID_NAME.test(name);
11662
- }
11663
12785
  function renderHandler() {
11664
12786
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
11665
12787
  //
@@ -11880,7 +13002,7 @@ Run \`primitive login\` once to save a key in your CLI config if you
11880
13002
  prefer that to an env var.
11881
13003
  `;
11882
13004
  }
11883
- function scaffoldFiles(name) {
13005
+ function renderEmailReplyTemplateFiles(name) {
11884
13006
  return [
11885
13007
  {
11886
13008
  contents: renderHandler(),
@@ -11908,9 +13030,79 @@ function scaffoldFiles(name) {
11908
13030
  }
11909
13031
  ];
11910
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
+ }
11911
13102
  function writeScaffold(params) {
11912
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 });
11913
- const files = scaffoldFiles(params.name);
13104
+ const templateId = params.templateId ?? "email-reply";
13105
+ const files = scaffoldFiles(params.name, templateId);
11914
13106
  const written = [];
11915
13107
  try {
11916
13108
  mkdirSync(params.outDir, { recursive: false });
@@ -11941,7 +13133,7 @@ function writeScaffold(params) {
11941
13133
  return { written };
11942
13134
  }
11943
13135
  var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11944
- static description = `Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.
13136
+ static description = `Scaffold a new Primitive Function project from a Primitive-owned template.
11945
13137
 
11946
13138
  The scaffolded handler imports \`createPrimitiveClient\` from
11947
13139
  \`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
@@ -11950,22 +13142,35 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11950
13142
  ./dist/handler.js, ready to hand to \`primitive functions deploy --file\`.
11951
13143
 
11952
13144
  Refuses to overwrite an existing directory. Use --out-dir to pick a
11953
- different target path than ./<name>/.`;
13145
+ different target path than ./<name>/. Run \`primitive functions templates\`
13146
+ to inspect available templates.`;
11954
13147
  static summary = "Scaffold a new Primitive Function project ready for functions deploy";
11955
- static examples = ["<%= config.bin %> functions init my-fn", "<%= config.bin %> functions init my-fn --out-dir ./functions/my-fn"];
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
+ ];
11956
13153
  static args = { name: Args.string({
11957
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.",
11958
13155
  required: true
11959
13156
  }) };
11960
- static flags = { "out-dir": Flags.string({ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist." }) };
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
+ };
11961
13165
  async run() {
11962
13166
  const { args, flags } = await this.parse(FunctionsInitCommand);
11963
13167
  const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
11964
13168
  writeScaffold({
11965
13169
  name: args.name,
11966
- outDir
13170
+ outDir,
13171
+ templateId: flags.template
11967
13172
  });
11968
- this.log(`Scaffolded ${outDir}.`);
13173
+ this.log(`Scaffolded ${outDir} from ${flags.template} template.`);
11969
13174
  this.log("Next:");
11970
13175
  this.log(` cd ${outDir}`);
11971
13176
  this.log(" npm install");
@@ -12046,17 +13251,19 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
12046
13251
  the bindings table fresh on every call, so passing the existing
12047
13252
  bundle picks up any secret writes since the last deploy.
12048
13253
 
12049
- Pass --secret KEY=VALUE (repeatable) to write secrets BEFORE the
12050
- redeploy fires; one update-function call then refreshes every new
12051
- binding. Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters,
12052
- digits, underscores; first character is a letter or underscore).
12053
- With one or more --secret flags the redeploy fans out to multiple
12054
- API calls (set-secret per pair, then update-function).`;
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}`;
12055
13260
  static summary = "Redeploy a function from a bundled handler file";
12056
13261
  static examples = [
12057
13262
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js",
12058
13263
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
12059
- "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com"
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"
12060
13267
  ];
12061
13268
  static flags = {
12062
13269
  "api-key": Flags.string({
@@ -12086,12 +13293,31 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
12086
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}`,
12087
13294
  multiple: true
12088
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." }),
12089
13309
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
12090
13310
  };
12091
13311
  async run() {
12092
13312
  const { flags } = await this.parse(FunctionsRedeployCommand);
12093
13313
  await runWithTiming(flags.time, async () => {
12094
- const parsedSecrets = parseSecretFlags(flags.secret ?? []);
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
+ });
12095
13321
  if (parsedSecrets.kind === "error") {
12096
13322
  process.stderr.write(`${parsedSecrets.message}\n`);
12097
13323
  process.exitCode = 1;
@@ -12242,9 +13468,15 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12242
13468
 
12243
13469
  Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
12244
13470
  underscores; first character is a letter or underscore). System-
12245
- managed keys are reserved and rejected.`;
13471
+ managed keys are reserved and rejected. ${SINGLE_SECRET_VALUE_SOURCE_DESCRIPTION}`;
12246
13472
  static summary = "Write a function secret (optionally redeploying to push it live)";
12247
- static examples = ["<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123", "<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy"];
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
+ ];
12248
13480
  static flags = {
12249
13481
  "api-key": Flags.string({
12250
13482
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
@@ -12268,10 +13500,11 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12268
13500
  description: "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
12269
13501
  required: true
12270
13502
  }),
12271
- value: Flags.string({
12272
- description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
12273
- required: true
12274
- }),
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" }),
12275
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." }),
12276
13509
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
12277
13510
  };
@@ -12295,6 +13528,19 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12295
13528
  baseUrlOverridden,
12296
13529
  configDir: this.config.configDir
12297
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
+ }
12298
13544
  const outcome = await runSetSecret({
12299
13545
  getFunction: (p) => getFunction({
12300
13546
  client: apiClient.client,
@@ -12320,7 +13566,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12320
13566
  id: flags.id,
12321
13567
  key: flags.key,
12322
13568
  redeploy: flags.redeploy === true,
12323
- value: flags.value
13569
+ value: resolvedValue.value
12324
13570
  });
12325
13571
  if (outcome.kind === "error") {
12326
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");
@@ -12338,9 +13584,111 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12338
13584
  }
12339
13585
  };
12340
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
12341
13609
  //#region src/oclif/commands/functions-test-function.ts
12342
13610
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
12343
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
+ }
12344
13692
  var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Command {
12345
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.";
12346
13694
  static summary = "Trigger a test invocation; with --wait, watch it land";
@@ -12424,6 +13772,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
12424
13772
  this.log(JSON.stringify(invocation, null, 2));
12425
13773
  return;
12426
13774
  }
13775
+ await maybeWriteEndpointNoiseWarning({
13776
+ apiClient,
13777
+ currentFunctionId: flags.id,
13778
+ invocation,
13779
+ writeStderr: (chunk) => {
13780
+ process.stderr.write(chunk);
13781
+ }
13782
+ });
12427
13783
  const startedAt = Date.now();
12428
13784
  const timeoutMs = flags.timeout * 1e3;
12429
13785
  const pollIntervalMs = flags["poll-interval"] * 1e3;
@@ -12486,17 +13842,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
12486
13842
  }
12487
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 });
12488
13844
  const elapsedSeconds = Math.round((Date.now() - startedAt) / 1e3);
12489
- const outcome = {
12490
- function_id: flags.id,
12491
- inbound_id: inboundId,
12492
- inbound_to: invocation.to,
12493
- webhook_status: detail.webhook_status,
12494
- webhook_attempt_count: detail.webhook_attempt_count,
12495
- webhook_last_status_code: detail.webhook_last_status_code,
12496
- webhook_last_error: detail.webhook_last_error,
12497
- elapsed_seconds: elapsedSeconds
12498
- };
12499
- 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
+ });
12500
13853
  this.log(JSON.stringify(outcome, null, 2));
12501
13854
  if (detail.webhook_status === "exhausted") process.exitCode = 1;
12502
13855
  });
@@ -13648,6 +15001,7 @@ const COMMANDS = {
13648
15001
  "emails:watch": EmailsWatchCommand,
13649
15002
  "emails:wait": EmailsWaitCommand,
13650
15003
  "functions:init": FunctionsInitCommand,
15004
+ "functions:templates": FunctionsTemplatesCommand,
13651
15005
  "functions:deploy": FunctionsDeployCommand,
13652
15006
  "functions:redeploy": FunctionsRedeployCommand,
13653
15007
  "functions:set-secret": FunctionsSetSecretCommand,