@primitivedotdev/cli 0.28.0 → 0.30.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,127 +5680,203 @@ 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"
5638
5699
  ]
5639
5700
  },
5640
- "FunctionLogRow": {
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": {
5641
5714
  "type": "object",
5642
- "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",
5643
5715
  "properties": {
5644
5716
  "id": {
5645
5717
  "type": "string",
5646
- "format": "uuid",
5647
- "description": "Unique log row id (stable across pages)."
5718
+ "format": "uuid"
5648
5719
  },
5649
5720
  "function_id": {
5650
5721
  "type": "string",
5651
- "format": "uuid",
5652
- "description": "The function this log row belongs to."
5722
+ "format": "uuid"
5653
5723
  },
5654
- "level": {
5724
+ "inbound_domain": { "type": "string" },
5725
+ "to": { "type": "string" },
5726
+ "from": { "type": "string" },
5727
+ "subject": { "type": "string" },
5728
+ "poll_since": {
5655
5729
  "type": "string",
5656
- "enum": [
5657
- "debug",
5658
- "log",
5659
- "info",
5660
- "warn",
5661
- "error"
5662
- ],
5663
- "description": "Severity. `log` is the runtime's default for unannotated\n`console.log` calls; the other levels match standard\n`console.*` methods.\n"
5730
+ "format": "date-time"
5664
5731
  },
5665
- "message": {
5732
+ "created_at": {
5666
5733
  "type": "string",
5667
- "description": "The textual message body. The runtime stringifies non-string\narguments before persisting, so this is always a plain\nstring.\n"
5734
+ "format": "date-time"
5668
5735
  },
5669
- "ts": {
5670
- "type": "string",
5671
- "format": "date-time",
5672
- "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"
5736
+ "sent_at": {
5737
+ "type": ["string", "null"],
5738
+ "format": "date-time"
5673
5739
  },
5674
- "metadata": {
5675
- "type": ["object", "null"],
5676
- "additionalProperties": true,
5677
- "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"
5678
- }
5740
+ "send_error": { "type": ["string", "null"] }
5679
5741
  },
5680
5742
  "required": [
5681
5743
  "id",
5682
5744
  "function_id",
5683
- "level",
5684
- "message",
5685
- "ts"
5745
+ "inbound_domain",
5746
+ "to",
5747
+ "from",
5748
+ "subject",
5749
+ "poll_since",
5750
+ "created_at",
5751
+ "sent_at",
5752
+ "send_error"
5686
5753
  ]
5687
5754
  },
5688
- "FunctionSecretListItem": {
5689
- "type": "object",
5690
- "description": "One row from GET /functions/{id}/secrets. Discriminate on the\n`managed` field:\n * `managed = true` — system secret provisioned by Primitive.\n `description` is set; `created_at` / `updated_at` are\n null because the row is virtual (resolved at deploy time\n from the managed registry, not stored in the secrets\n table).\n * `managed = false` — secret the user set via the API.\n `created_at` / `updated_at` are set; `description` is\n null.\n",
5755
+ "FunctionTestRunSend": {
5756
+ "type": ["object", "null"],
5691
5757
  "properties": {
5692
- "key": { "type": "string" },
5693
- "managed": {
5694
- "type": "boolean",
5695
- "description": "True for managed system secrets, false for user-set entries."
5696
- },
5697
- "description": {
5698
- "type": ["string", "null"],
5699
- "description": "Set on managed entries only; null on user-set entries."
5758
+ "id": {
5759
+ "type": "string",
5760
+ "format": "uuid"
5700
5761
  },
5762
+ "status": { "$ref": "#/components/schemas/SentEmailStatus" },
5763
+ "queue_id": { "type": ["string", "null"] },
5701
5764
  "created_at": {
5702
- "type": ["string", "null"],
5703
- "format": "date-time",
5704
- "description": "Set on user-set entries only; null on managed entries."
5765
+ "type": "string",
5766
+ "format": "date-time"
5705
5767
  },
5706
5768
  "updated_at": {
5707
- "type": ["string", "null"],
5708
- "format": "date-time",
5709
- "description": "Set on user-set entries only; null on managed entries."
5769
+ "type": "string",
5770
+ "format": "date-time"
5710
5771
  }
5711
5772
  },
5712
- "required": ["key", "managed"]
5773
+ "required": [
5774
+ "id",
5775
+ "status",
5776
+ "queue_id",
5777
+ "created_at",
5778
+ "updated_at"
5779
+ ]
5713
5780
  },
5714
- "CreateFunctionSecretInput": {
5715
- "type": "object",
5716
- "additionalProperties": false,
5717
- "description": "Body for POST /functions/{id}/secrets.",
5781
+ "FunctionTestRunInboundEmail": {
5782
+ "type": ["object", "null"],
5718
5783
  "properties": {
5719
- "key": {
5784
+ "id": {
5720
5785
  "type": "string",
5721
- "pattern": "^[A-Z_][A-Z0-9_]*$",
5722
- "description": "Uppercase letters, digits, and underscores. Must start with\na letter or underscore. System-managed keys (e.g.\nPRIMITIVE_WEBHOOK_SECRET) are reserved.\n"
5786
+ "format": "uuid"
5723
5787
  },
5724
- "value": {
5788
+ "status": { "$ref": "#/components/schemas/EmailStatus" },
5789
+ "received_at": {
5725
5790
  "type": "string",
5726
- "minLength": 1,
5727
- "maxLength": 4096,
5728
- "description": "Secret value, up to 4096 UTF-8 bytes. Encrypted at rest.\nNever returned by any read endpoint.\n"
5729
- }
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"] }
5730
5800
  },
5731
- "required": ["key", "value"]
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
+ ]
5732
5813
  },
5733
- "SetFunctionSecretInput": {
5734
- "type": "object",
5735
- "additionalProperties": false,
5736
- "description": "Body for PUT /functions/{id}/secrets/{key}. Key comes from the path.",
5737
- "properties": { "value": {
5738
- "type": "string",
5739
- "minLength": 1,
5740
- "maxLength": 4096
5741
- } },
5742
- "required": ["value"]
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
+ ]
5743
5851
  },
5744
- "FunctionSecretWriteResult": {
5852
+ "FunctionTestRunDelivery": {
5745
5853
  "type": "object",
5746
- "description": "Returned by POST and PUT secret routes.",
5747
5854
  "properties": {
5748
- "key": { "type": "string" },
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"] },
5749
5880
  "created_at": {
5750
5881
  "type": "string",
5751
5882
  "format": "date-time"
@@ -5754,48 +5885,295 @@ const openapiDocument = {
5754
5885
  "type": "string",
5755
5886
  "format": "date-time"
5756
5887
  },
5757
- "created": {
5758
- "type": "boolean",
5759
- "description": "True if this call inserted a new row, false if it updated an existing one."
5760
- }
5888
+ "endpoint": { "$ref": "#/components/schemas/FunctionTestRunDeliveryEndpoint" }
5761
5889
  },
5762
5890
  "required": [
5763
- "key",
5891
+ "id",
5892
+ "endpoint_id",
5893
+ "endpoint_url",
5894
+ "status",
5895
+ "attempt_count",
5896
+ "duration_ms",
5897
+ "last_error",
5898
+ "last_error_code",
5764
5899
  "created_at",
5765
5900
  "updated_at",
5766
- "created"
5901
+ "endpoint"
5767
5902
  ]
5768
- }
5769
- }
5770
- }
5771
- };
5772
- //#endregion
5773
- //#region ../packages/api-core/src/openapi/operations.generated.ts
5774
- const operationManifest = [
5775
- {
5776
- "binaryResponse": false,
5777
- "bodyRequired": false,
5778
- "command": "get-account",
5779
- "description": null,
5780
- "hasJsonBody": false,
5781
- "method": "GET",
5782
- "operationId": "getAccount",
5783
- "path": "/account",
5784
- "pathParams": [],
5785
- "queryParams": [],
5786
- "requestSchema": null,
5787
- "responseSchema": {
5788
- "type": "object",
5789
- "properties": {
5790
- "id": {
5791
- "type": "string",
5792
- "format": "uuid"
5793
- },
5794
- "email": { "type": "string" },
5795
- "plan": { "type": "string" },
5796
- "created_at": {
5797
- "type": "string",
5798
- "format": "date-time"
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"
6016
+ ]
6017
+ },
6018
+ "FunctionLogRow": {
6019
+ "type": "object",
6020
+ "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",
6021
+ "properties": {
6022
+ "id": {
6023
+ "type": "string",
6024
+ "format": "uuid",
6025
+ "description": "Unique log row id (stable across pages)."
6026
+ },
6027
+ "function_id": {
6028
+ "type": "string",
6029
+ "format": "uuid",
6030
+ "description": "The function this log row belongs to."
6031
+ },
6032
+ "level": {
6033
+ "type": "string",
6034
+ "enum": [
6035
+ "debug",
6036
+ "log",
6037
+ "info",
6038
+ "warn",
6039
+ "error"
6040
+ ],
6041
+ "description": "Severity. `log` is the runtime's default for unannotated\n`console.log` calls; the other levels match standard\n`console.*` methods.\n"
6042
+ },
6043
+ "message": {
6044
+ "type": "string",
6045
+ "description": "The textual message body. The runtime stringifies non-string\narguments before persisting, so this is always a plain\nstring.\n"
6046
+ },
6047
+ "ts": {
6048
+ "type": "string",
6049
+ "format": "date-time",
6050
+ "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"
6051
+ },
6052
+ "metadata": {
6053
+ "type": ["object", "null"],
6054
+ "additionalProperties": true,
6055
+ "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"
6056
+ }
6057
+ },
6058
+ "required": [
6059
+ "id",
6060
+ "function_id",
6061
+ "level",
6062
+ "message",
6063
+ "ts"
6064
+ ]
6065
+ },
6066
+ "FunctionSecretListItem": {
6067
+ "type": "object",
6068
+ "description": "One row from GET /functions/{id}/secrets. Discriminate on the\n`managed` field:\n * `managed = true` — system secret provisioned by Primitive.\n `description` is set; `created_at` / `updated_at` are\n null because the row is virtual (resolved at deploy time\n from the managed registry, not stored in the secrets\n table).\n * `managed = false` — secret the user set via the API.\n `created_at` / `updated_at` are set; `description` is\n null.\n",
6069
+ "properties": {
6070
+ "key": { "type": "string" },
6071
+ "managed": {
6072
+ "type": "boolean",
6073
+ "description": "True for managed system secrets, false for user-set entries."
6074
+ },
6075
+ "description": {
6076
+ "type": ["string", "null"],
6077
+ "description": "Set on managed entries only; null on user-set entries."
6078
+ },
6079
+ "created_at": {
6080
+ "type": ["string", "null"],
6081
+ "format": "date-time",
6082
+ "description": "Set on user-set entries only; null on managed entries."
6083
+ },
6084
+ "updated_at": {
6085
+ "type": ["string", "null"],
6086
+ "format": "date-time",
6087
+ "description": "Set on user-set entries only; null on managed entries."
6088
+ }
6089
+ },
6090
+ "required": ["key", "managed"]
6091
+ },
6092
+ "CreateFunctionSecretInput": {
6093
+ "type": "object",
6094
+ "additionalProperties": false,
6095
+ "description": "Body for POST /functions/{id}/secrets.",
6096
+ "properties": {
6097
+ "key": {
6098
+ "type": "string",
6099
+ "pattern": "^[A-Z_][A-Z0-9_]*$",
6100
+ "description": "Uppercase letters, digits, and underscores. Must start with\na letter or underscore. System-managed keys (e.g.\nPRIMITIVE_WEBHOOK_SECRET) are reserved.\n"
6101
+ },
6102
+ "value": {
6103
+ "type": "string",
6104
+ "minLength": 1,
6105
+ "maxLength": 4096,
6106
+ "description": "Secret value, up to 4096 UTF-8 bytes. Encrypted at rest.\nNever returned by any read endpoint.\n"
6107
+ }
6108
+ },
6109
+ "required": ["key", "value"]
6110
+ },
6111
+ "SetFunctionSecretInput": {
6112
+ "type": "object",
6113
+ "additionalProperties": false,
6114
+ "description": "Body for PUT /functions/{id}/secrets/{key}. Key comes from the path.",
6115
+ "properties": { "value": {
6116
+ "type": "string",
6117
+ "minLength": 1,
6118
+ "maxLength": 4096
6119
+ } },
6120
+ "required": ["value"]
6121
+ },
6122
+ "FunctionSecretWriteResult": {
6123
+ "type": "object",
6124
+ "description": "Returned by POST and PUT secret routes.",
6125
+ "properties": {
6126
+ "key": { "type": "string" },
6127
+ "created_at": {
6128
+ "type": "string",
6129
+ "format": "date-time"
6130
+ },
6131
+ "updated_at": {
6132
+ "type": "string",
6133
+ "format": "date-time"
6134
+ },
6135
+ "created": {
6136
+ "type": "boolean",
6137
+ "description": "True if this call inserted a new row, false if it updated an existing one."
6138
+ }
6139
+ },
6140
+ "required": [
6141
+ "key",
6142
+ "created_at",
6143
+ "updated_at",
6144
+ "created"
6145
+ ]
6146
+ }
6147
+ }
6148
+ }
6149
+ };
6150
+ //#endregion
6151
+ //#region ../packages/api-core/src/openapi/operations.generated.ts
6152
+ const operationManifest = [
6153
+ {
6154
+ "binaryResponse": false,
6155
+ "bodyRequired": false,
6156
+ "command": "get-account",
6157
+ "description": null,
6158
+ "hasJsonBody": false,
6159
+ "method": "GET",
6160
+ "operationId": "getAccount",
6161
+ "path": "/account",
6162
+ "pathParams": [],
6163
+ "queryParams": [],
6164
+ "requestSchema": null,
6165
+ "responseSchema": {
6166
+ "type": "object",
6167
+ "properties": {
6168
+ "id": {
6169
+ "type": "string",
6170
+ "format": "uuid"
6171
+ },
6172
+ "email": { "type": "string" },
6173
+ "plan": { "type": "string" },
6174
+ "created_at": {
6175
+ "type": "string",
6176
+ "format": "date-time"
5799
6177
  },
5800
6178
  "onboarding_completed": { "type": "boolean" },
5801
6179
  "onboarding_step": { "type": ["string", "null"] },
@@ -6997,8 +7375,11 @@ const operationManifest = [
6997
7375
  "type": "string"
6998
7376
  },
6999
7377
  {
7378
+ "default": 50,
7000
7379
  "description": "Number of results per page",
7001
7380
  "enum": null,
7381
+ "maximum": 100,
7382
+ "minimum": 1,
7002
7383
  "name": "limit",
7003
7384
  "required": false,
7004
7385
  "type": "integer"
@@ -7267,13 +7648,17 @@ const operationManifest = [
7267
7648
  "type": "string"
7268
7649
  },
7269
7650
  {
7651
+ "default": 50,
7270
7652
  "description": "Number of results per page",
7271
7653
  "enum": null,
7654
+ "maximum": 100,
7655
+ "minimum": 1,
7272
7656
  "name": "limit",
7273
7657
  "required": false,
7274
7658
  "type": "integer"
7275
7659
  },
7276
7660
  {
7661
+ "default": "true",
7277
7662
  "description": "Include subject/body highlight snippets when text search is active.",
7278
7663
  "enum": ["true", "false"],
7279
7664
  "name": "snippet",
@@ -7281,6 +7666,7 @@ const operationManifest = [
7281
7666
  "type": "string"
7282
7667
  },
7283
7668
  {
7669
+ "default": "true",
7284
7670
  "description": "Include facet counts for sender, domain, status, and attachment presence.",
7285
7671
  "enum": ["true", "false"],
7286
7672
  "name": "include_facets",
@@ -8289,43 +8675,479 @@ const operationManifest = [
8289
8675
  {
8290
8676
  "binaryResponse": false,
8291
8677
  "bodyRequired": false,
8292
- "command": "list-function-logs",
8293
- "description": "Returns the most recent `function_logs` rows for the function,\nnewest first. Each row is a single `console.log` / `console.error`\ninvocation captured from the running handler.\n\nPage through history with the opaque `cursor` returned as\n`next_cursor`; pass it back as the `cursor` query param on the\nnext call. `next_cursor` is `null` when there are no further\nrows. The cursor format is an implementation detail and should\nnot be parsed by callers.\n",
8678
+ "command": "get-function-test-run-trace",
8679
+ "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",
8294
8680
  "hasJsonBody": false,
8295
8681
  "method": "GET",
8296
- "operationId": "listFunctionLogs",
8297
- "path": "/functions/{id}/logs",
8682
+ "operationId": "getFunctionTestRunTrace",
8683
+ "path": "/functions/{id}/test-runs/{run_id}/trace",
8298
8684
  "pathParams": [{
8299
8685
  "description": "Resource UUID",
8300
8686
  "enum": null,
8301
8687
  "name": "id",
8302
8688
  "required": true,
8303
8689
  "type": "string"
8304
- }],
8305
- "queryParams": [{
8306
- "description": "Maximum number of rows to return. Clamped to 1..200; default\n50.\n",
8307
- "enum": null,
8308
- "name": "limit",
8309
- "required": false,
8310
- "type": "integer"
8311
8690
  }, {
8312
- "description": "Opaque pagination cursor from a previous response's\n`next_cursor`. Omit on the first call.\n",
8691
+ "description": "Function test run id returned by POST /functions/{id}/test.",
8313
8692
  "enum": null,
8314
- "name": "cursor",
8315
- "required": false,
8693
+ "name": "run_id",
8694
+ "required": true,
8316
8695
  "type": "string"
8317
8696
  }],
8697
+ "queryParams": [],
8318
8698
  "requestSchema": null,
8319
8699
  "responseSchema": {
8320
8700
  "type": "object",
8701
+ "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",
8321
8702
  "properties": {
8322
- "items": {
8323
- "type": "array",
8324
- "items": {
8325
- "type": "object",
8326
- "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",
8327
- "properties": {
8328
- "id": {
8703
+ "state": {
8704
+ "type": "string",
8705
+ "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",
8706
+ "enum": [
8707
+ "send_failed",
8708
+ "waiting_for_send",
8709
+ "waiting_for_inbound",
8710
+ "waiting_for_function",
8711
+ "completed",
8712
+ "failed"
8713
+ ]
8714
+ },
8715
+ "test_run": {
8716
+ "type": "object",
8717
+ "properties": {
8718
+ "id": {
8719
+ "type": "string",
8720
+ "format": "uuid"
8721
+ },
8722
+ "function_id": {
8723
+ "type": "string",
8724
+ "format": "uuid"
8725
+ },
8726
+ "inbound_domain": { "type": "string" },
8727
+ "to": { "type": "string" },
8728
+ "from": { "type": "string" },
8729
+ "subject": { "type": "string" },
8730
+ "poll_since": {
8731
+ "type": "string",
8732
+ "format": "date-time"
8733
+ },
8734
+ "created_at": {
8735
+ "type": "string",
8736
+ "format": "date-time"
8737
+ },
8738
+ "sent_at": {
8739
+ "type": ["string", "null"],
8740
+ "format": "date-time"
8741
+ },
8742
+ "send_error": { "type": ["string", "null"] }
8743
+ },
8744
+ "required": [
8745
+ "id",
8746
+ "function_id",
8747
+ "inbound_domain",
8748
+ "to",
8749
+ "from",
8750
+ "subject",
8751
+ "poll_since",
8752
+ "created_at",
8753
+ "sent_at",
8754
+ "send_error"
8755
+ ]
8756
+ },
8757
+ "test_send": {
8758
+ "type": ["object", "null"],
8759
+ "properties": {
8760
+ "id": {
8761
+ "type": "string",
8762
+ "format": "uuid"
8763
+ },
8764
+ "status": {
8765
+ "type": "string",
8766
+ "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",
8767
+ "enum": [
8768
+ "queued",
8769
+ "submitted_to_agent",
8770
+ "agent_failed",
8771
+ "gate_denied",
8772
+ "unknown",
8773
+ "delivered",
8774
+ "bounced",
8775
+ "deferred",
8776
+ "wait_timeout"
8777
+ ]
8778
+ },
8779
+ "queue_id": { "type": ["string", "null"] },
8780
+ "created_at": {
8781
+ "type": "string",
8782
+ "format": "date-time"
8783
+ },
8784
+ "updated_at": {
8785
+ "type": "string",
8786
+ "format": "date-time"
8787
+ }
8788
+ },
8789
+ "required": [
8790
+ "id",
8791
+ "status",
8792
+ "queue_id",
8793
+ "created_at",
8794
+ "updated_at"
8795
+ ]
8796
+ },
8797
+ "inbound_email": {
8798
+ "type": ["object", "null"],
8799
+ "properties": {
8800
+ "id": {
8801
+ "type": "string",
8802
+ "format": "uuid"
8803
+ },
8804
+ "status": {
8805
+ "type": "string",
8806
+ "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",
8807
+ "enum": [
8808
+ "pending",
8809
+ "accepted",
8810
+ "completed",
8811
+ "rejected"
8812
+ ]
8813
+ },
8814
+ "received_at": {
8815
+ "type": "string",
8816
+ "format": "date-time"
8817
+ },
8818
+ "from": { "type": "string" },
8819
+ "to": { "type": "string" },
8820
+ "subject": { "type": ["string", "null"] },
8821
+ "webhook_status": {
8822
+ "type": ["string", "null"],
8823
+ "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",
8824
+ "enum": [
8825
+ "pending",
8826
+ "in_flight",
8827
+ "fired",
8828
+ "failed",
8829
+ "exhausted",
8830
+ null
8831
+ ]
8832
+ },
8833
+ "webhook_attempt_count": { "type": "integer" },
8834
+ "webhook_last_status_code": { "type": ["integer", "null"] },
8835
+ "webhook_last_error": { "type": ["string", "null"] }
8836
+ },
8837
+ "required": [
8838
+ "id",
8839
+ "status",
8840
+ "received_at",
8841
+ "from",
8842
+ "to",
8843
+ "subject",
8844
+ "webhook_status",
8845
+ "webhook_attempt_count",
8846
+ "webhook_last_status_code",
8847
+ "webhook_last_error"
8848
+ ]
8849
+ },
8850
+ "deliveries": {
8851
+ "type": "array",
8852
+ "items": {
8853
+ "type": "object",
8854
+ "properties": {
8855
+ "id": {
8856
+ "type": "string",
8857
+ "description": "Webhook delivery id."
8858
+ },
8859
+ "endpoint_id": {
8860
+ "type": "string",
8861
+ "format": "uuid"
8862
+ },
8863
+ "endpoint_url": {
8864
+ "type": "string",
8865
+ "format": "uri"
8866
+ },
8867
+ "status": {
8868
+ "type": "string",
8869
+ "enum": [
8870
+ "pending",
8871
+ "delivered",
8872
+ "header_confirmed",
8873
+ "failed"
8874
+ ]
8875
+ },
8876
+ "attempt_count": { "type": "integer" },
8877
+ "duration_ms": { "type": ["integer", "null"] },
8878
+ "last_error": { "type": ["string", "null"] },
8879
+ "last_error_code": { "type": ["string", "null"] },
8880
+ "created_at": {
8881
+ "type": "string",
8882
+ "format": "date-time"
8883
+ },
8884
+ "updated_at": {
8885
+ "type": "string",
8886
+ "format": "date-time"
8887
+ },
8888
+ "endpoint": {
8889
+ "type": ["object", "null"],
8890
+ "properties": {
8891
+ "id": {
8892
+ "type": "string",
8893
+ "format": "uuid"
8894
+ },
8895
+ "kind": {
8896
+ "type": "string",
8897
+ "description": "Endpoint kind. Current traces may include `http` or `function`; future endpoint kinds may appear."
8898
+ },
8899
+ "function_id": {
8900
+ "type": ["string", "null"],
8901
+ "format": "uuid"
8902
+ },
8903
+ "function_name": { "type": ["string", "null"] },
8904
+ "domain_id": {
8905
+ "type": ["string", "null"],
8906
+ "format": "uuid"
8907
+ },
8908
+ "enabled": { "type": "boolean" },
8909
+ "deactivated_at": {
8910
+ "type": ["string", "null"],
8911
+ "format": "date-time"
8912
+ },
8913
+ "is_current_function": { "type": "boolean" }
8914
+ },
8915
+ "required": [
8916
+ "id",
8917
+ "kind",
8918
+ "function_id",
8919
+ "function_name",
8920
+ "domain_id",
8921
+ "enabled",
8922
+ "deactivated_at",
8923
+ "is_current_function"
8924
+ ]
8925
+ }
8926
+ },
8927
+ "required": [
8928
+ "id",
8929
+ "endpoint_id",
8930
+ "endpoint_url",
8931
+ "status",
8932
+ "attempt_count",
8933
+ "duration_ms",
8934
+ "last_error",
8935
+ "last_error_code",
8936
+ "created_at",
8937
+ "updated_at",
8938
+ "endpoint"
8939
+ ]
8940
+ }
8941
+ },
8942
+ "outbound_requests": {
8943
+ "type": "array",
8944
+ "items": {
8945
+ "type": "object",
8946
+ "properties": {
8947
+ "id": {
8948
+ "type": "string",
8949
+ "format": "uuid"
8950
+ },
8951
+ "function_id": {
8952
+ "type": "string",
8953
+ "format": "uuid"
8954
+ },
8955
+ "webhook_delivery_id": { "type": ["string", "null"] },
8956
+ "email_id": {
8957
+ "type": ["string", "null"],
8958
+ "format": "uuid"
8959
+ },
8960
+ "endpoint_id": {
8961
+ "type": ["string", "null"],
8962
+ "format": "uuid"
8963
+ },
8964
+ "method": { "type": "string" },
8965
+ "url": {
8966
+ "type": "string",
8967
+ "format": "uri"
8968
+ },
8969
+ "host": { "type": "string" },
8970
+ "path": { "type": "string" },
8971
+ "status_code": { "type": ["integer", "null"] },
8972
+ "ok": { "type": ["boolean", "null"] },
8973
+ "duration_ms": { "type": "integer" },
8974
+ "error": { "type": ["string", "null"] },
8975
+ "ts": {
8976
+ "type": "string",
8977
+ "format": "date-time"
8978
+ }
8979
+ },
8980
+ "required": [
8981
+ "id",
8982
+ "function_id",
8983
+ "webhook_delivery_id",
8984
+ "email_id",
8985
+ "endpoint_id",
8986
+ "method",
8987
+ "url",
8988
+ "host",
8989
+ "path",
8990
+ "status_code",
8991
+ "ok",
8992
+ "duration_ms",
8993
+ "error",
8994
+ "ts"
8995
+ ]
8996
+ }
8997
+ },
8998
+ "logs": {
8999
+ "type": "array",
9000
+ "items": {
9001
+ "type": "object",
9002
+ "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",
9003
+ "properties": {
9004
+ "id": {
9005
+ "type": "string",
9006
+ "format": "uuid",
9007
+ "description": "Unique log row id (stable across pages)."
9008
+ },
9009
+ "function_id": {
9010
+ "type": "string",
9011
+ "format": "uuid",
9012
+ "description": "The function this log row belongs to."
9013
+ },
9014
+ "level": {
9015
+ "type": "string",
9016
+ "enum": [
9017
+ "debug",
9018
+ "log",
9019
+ "info",
9020
+ "warn",
9021
+ "error"
9022
+ ],
9023
+ "description": "Severity. `log` is the runtime's default for unannotated\n`console.log` calls; the other levels match standard\n`console.*` methods.\n"
9024
+ },
9025
+ "message": {
9026
+ "type": "string",
9027
+ "description": "The textual message body. The runtime stringifies non-string\narguments before persisting, so this is always a plain\nstring.\n"
9028
+ },
9029
+ "ts": {
9030
+ "type": "string",
9031
+ "format": "date-time",
9032
+ "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"
9033
+ },
9034
+ "metadata": {
9035
+ "type": ["object", "null"],
9036
+ "additionalProperties": true,
9037
+ "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"
9038
+ }
9039
+ },
9040
+ "required": [
9041
+ "id",
9042
+ "function_id",
9043
+ "level",
9044
+ "message",
9045
+ "ts"
9046
+ ]
9047
+ }
9048
+ },
9049
+ "replies": {
9050
+ "type": "array",
9051
+ "items": {
9052
+ "type": "object",
9053
+ "properties": {
9054
+ "id": {
9055
+ "type": "string",
9056
+ "format": "uuid"
9057
+ },
9058
+ "status": {
9059
+ "type": "string",
9060
+ "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",
9061
+ "enum": [
9062
+ "queued",
9063
+ "submitted_to_agent",
9064
+ "agent_failed",
9065
+ "gate_denied",
9066
+ "unknown",
9067
+ "delivered",
9068
+ "bounced",
9069
+ "deferred",
9070
+ "wait_timeout"
9071
+ ]
9072
+ },
9073
+ "to": { "type": "string" },
9074
+ "subject": { "type": "string" },
9075
+ "queue_id": { "type": ["string", "null"] },
9076
+ "created_at": {
9077
+ "type": "string",
9078
+ "format": "date-time"
9079
+ }
9080
+ },
9081
+ "required": [
9082
+ "id",
9083
+ "status",
9084
+ "to",
9085
+ "subject",
9086
+ "queue_id",
9087
+ "created_at"
9088
+ ]
9089
+ }
9090
+ }
9091
+ },
9092
+ "required": [
9093
+ "state",
9094
+ "test_run",
9095
+ "test_send",
9096
+ "inbound_email",
9097
+ "deliveries",
9098
+ "outbound_requests",
9099
+ "logs",
9100
+ "replies"
9101
+ ]
9102
+ },
9103
+ "sdkName": "getFunctionTestRunTrace",
9104
+ "summary": "Get a function test run trace",
9105
+ "tag": "Functions",
9106
+ "tagCommand": "functions"
9107
+ },
9108
+ {
9109
+ "binaryResponse": false,
9110
+ "bodyRequired": false,
9111
+ "command": "list-function-logs",
9112
+ "description": "Returns the most recent `function_logs` rows for the function,\nnewest first. Each row is a single `console.log` / `console.error`\ninvocation captured from the running handler.\n\nPage through history with the opaque `cursor` returned as\n`next_cursor`; pass it back as the `cursor` query param on the\nnext call. `next_cursor` is `null` when there are no further\nrows. The cursor format is an implementation detail and should\nnot be parsed by callers.\n",
9113
+ "hasJsonBody": false,
9114
+ "method": "GET",
9115
+ "operationId": "listFunctionLogs",
9116
+ "path": "/functions/{id}/logs",
9117
+ "pathParams": [{
9118
+ "description": "Resource UUID",
9119
+ "enum": null,
9120
+ "name": "id",
9121
+ "required": true,
9122
+ "type": "string"
9123
+ }],
9124
+ "queryParams": [{
9125
+ "default": 50,
9126
+ "description": "Maximum number of rows to return. Clamped to 1..200; default\n50.\n",
9127
+ "enum": null,
9128
+ "maximum": 200,
9129
+ "minimum": 1,
9130
+ "name": "limit",
9131
+ "required": false,
9132
+ "type": "integer"
9133
+ }, {
9134
+ "description": "Opaque pagination cursor from a previous response's\n`next_cursor`. Omit on the first call.\n",
9135
+ "enum": null,
9136
+ "name": "cursor",
9137
+ "required": false,
9138
+ "type": "string"
9139
+ }],
9140
+ "requestSchema": null,
9141
+ "responseSchema": {
9142
+ "type": "object",
9143
+ "properties": {
9144
+ "items": {
9145
+ "type": "array",
9146
+ "items": {
9147
+ "type": "object",
9148
+ "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",
9149
+ "properties": {
9150
+ "id": {
8329
9151
  "type": "string",
8330
9152
  "format": "uuid",
8331
9153
  "description": "Unique log row id (stable across pages)."
@@ -8602,8 +9424,13 @@ const operationManifest = [
8602
9424
  },
8603
9425
  "responseSchema": {
8604
9426
  "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",
9427
+ "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
9428
  "properties": {
9429
+ "test_run_id": {
9430
+ "type": "string",
9431
+ "format": "uuid",
9432
+ "description": "Durable test run id used to fetch the run trace."
9433
+ },
8607
9434
  "inbound_domain": {
8608
9435
  "type": "string",
8609
9436
  "description": "Verified inbound domain the test email was sent to."
@@ -8633,16 +9460,22 @@ const operationManifest = [
8633
9460
  "type": "string",
8634
9461
  "format": "uri",
8635
9462
  "description": "Function detail page where invocations show up live."
9463
+ },
9464
+ "trace_url": {
9465
+ "type": "string",
9466
+ "description": "Relative API URL for GET /functions/{id}/test-runs/{test_run_id}/trace."
8636
9467
  }
8637
9468
  },
8638
9469
  "required": [
9470
+ "test_run_id",
8639
9471
  "inbound_domain",
8640
9472
  "to",
8641
9473
  "from",
8642
9474
  "send_id",
8643
9475
  "subject",
8644
9476
  "poll_since",
8645
- "watch_url"
9477
+ "watch_url",
9478
+ "trace_url"
8646
9479
  ]
8647
9480
  },
8648
9481
  "sdkName": "testFunction",
@@ -9128,8 +9961,11 @@ const operationManifest = [
9128
9961
  "type": "string"
9129
9962
  },
9130
9963
  {
9964
+ "default": 50,
9131
9965
  "description": "Number of results per page",
9132
9966
  "enum": null,
9967
+ "maximum": 100,
9968
+ "minimum": 1,
9133
9969
  "name": "limit",
9134
9970
  "required": false,
9135
9971
  "type": "integer"
@@ -9701,8 +10537,11 @@ const operationManifest = [
9701
10537
  "type": "string"
9702
10538
  },
9703
10539
  {
10540
+ "default": 50,
9704
10541
  "description": "Number of results per page",
9705
10542
  "enum": null,
10543
+ "maximum": 100,
10544
+ "minimum": 1,
9706
10545
  "name": "limit",
9707
10546
  "required": false,
9708
10547
  "type": "integer"
@@ -10166,6 +11005,22 @@ function flagName(parameterName) {
10166
11005
  function flagDescription(parameter) {
10167
11006
  return parameter.description ?? parameter.name;
10168
11007
  }
11008
+ const numberFlag = Flags.custom({ async parse(input, _context, options) {
11009
+ const trimmed = input.trim();
11010
+ if (trimmed === "") throw new Errors.CLIError(`Expected a number but received: ${input}`);
11011
+ const value = Number(trimmed);
11012
+ if (!Number.isFinite(value)) throw new Errors.CLIError(`Expected a number but received: ${input}`);
11013
+ if (options.min !== void 0 && value < options.min) throw new Errors.CLIError(`Expected a number greater than or equal to ${options.min} but received: ${input}`);
11014
+ if (options.max !== void 0 && value > options.max) throw new Errors.CLIError(`Expected a number less than or equal to ${options.max} but received: ${input}`);
11015
+ return value;
11016
+ } });
11017
+ function numericFlagOptions(parameter) {
11018
+ return {
11019
+ ...typeof parameter.default === "number" ? { default: parameter.default } : {},
11020
+ ...typeof parameter.maximum === "number" ? { max: parameter.maximum } : {},
11021
+ ...typeof parameter.minimum === "number" ? { min: parameter.minimum } : {}
11022
+ };
11023
+ }
10169
11024
  function extractBodyFields(schema) {
10170
11025
  if (!schema || typeof schema !== "object") return [];
10171
11026
  const properties = schema.properties;
@@ -10181,7 +11036,8 @@ function extractBodyFields(schema) {
10181
11036
  if (typeof t === "string") {
10182
11037
  displayType = t;
10183
11038
  if (t === "string") kind = "string";
10184
- else if (t === "integer" || t === "number") kind = "integer";
11039
+ else if (t === "integer") kind = "integer";
11040
+ else if (t === "number") kind = "number";
10185
11041
  else if (t === "boolean") kind = "boolean";
10186
11042
  else if (t === "array") {
10187
11043
  const items = propSchema.items;
@@ -10197,7 +11053,8 @@ function extractBodyFields(schema) {
10197
11053
  const single = nonNull[0];
10198
11054
  displayType = `${single}?`;
10199
11055
  if (single === "string") kind = "string";
10200
- else if (single === "integer" || single === "number") kind = "integer";
11056
+ else if (single === "integer") kind = "integer";
11057
+ else if (single === "number") kind = "number";
10201
11058
  else if (single === "boolean") kind = "boolean";
10202
11059
  else kind = "complex";
10203
11060
  } else {
@@ -10214,7 +11071,9 @@ function extractBodyFields(schema) {
10214
11071
  required: required.has(name),
10215
11072
  displayType,
10216
11073
  kind,
10217
- ...enumValues && enumValues.length > 0 ? { enumValues } : {}
11074
+ ...enumValues && enumValues.length > 0 ? { enumValues } : {},
11075
+ ...typeof propSchema.maximum === "number" ? { maximum: propSchema.maximum } : {},
11076
+ ...typeof propSchema.minimum === "number" ? { minimum: propSchema.minimum } : {}
10218
11077
  });
10219
11078
  }
10220
11079
  return fields.sort((a, b) => {
@@ -10262,7 +11121,14 @@ function flagForParameter(parameter) {
10262
11121
  required: parameter.required
10263
11122
  };
10264
11123
  if (parameter.type === "boolean") return Flags.boolean(common);
10265
- if (parameter.type === "integer") return Flags.integer(common);
11124
+ if (parameter.type === "integer") return Flags.integer({
11125
+ ...common,
11126
+ ...numericFlagOptions(parameter)
11127
+ });
11128
+ if (parameter.type === "number") return numberFlag({
11129
+ ...common,
11130
+ ...numericFlagOptions(parameter)
11131
+ });
10266
11132
  if (parameter.enum && parameter.enum.length > 0) return Flags.string({
10267
11133
  ...common,
10268
11134
  options: parameter.enum
@@ -10412,12 +11278,20 @@ const RESERVED_FLAG_NAMES = new Set([
10412
11278
  "api-base-url-2",
10413
11279
  "raw-body",
10414
11280
  "body-file",
11281
+ "envelope",
10415
11282
  "output"
10416
11283
  ]);
10417
11284
  function bodyFieldFlag(field) {
10418
11285
  const common = { description: field.description || field.name };
10419
11286
  if (field.kind === "boolean") return Flags.boolean(common);
10420
- if (field.kind === "integer") return Flags.integer(common);
11287
+ if (field.kind === "integer") return Flags.integer({
11288
+ ...common,
11289
+ ...numericFlagOptions(field)
11290
+ });
11291
+ if (field.kind === "number") return numberFlag({
11292
+ ...common,
11293
+ ...numericFlagOptions(field)
11294
+ });
10421
11295
  if (field.enumValues) return Flags.string({
10422
11296
  ...common,
10423
11297
  options: field.enumValues
@@ -10442,6 +11316,7 @@ function buildFlags(operation) {
10442
11316
  }),
10443
11317
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
10444
11318
  };
11319
+ if (!operation.binaryResponse) flags.envelope = Flags.boolean({ description: "Print the full response envelope, including pagination metadata such as meta.cursor. Defaults to printing only the data payload for backward compatibility." });
10445
11320
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) flags[flagName(parameter.name)] = flagForParameter(parameter);
10446
11321
  const bodyFieldFlagToProperty = /* @__PURE__ */ new Map();
10447
11322
  if (operation.hasJsonBody) {
@@ -10480,6 +11355,9 @@ function collectValues(parameters, flags) {
10480
11355
  }
10481
11356
  return values;
10482
11357
  }
11358
+ function operationOutputPayload(envelope, includeEnvelope) {
11359
+ return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
11360
+ }
10483
11361
  const OPERATION_HINTS = {
10484
11362
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
10485
11363
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
@@ -10587,7 +11465,7 @@ function createOperationCommand(operation) {
10587
11465
  writeIdempotentReplayBannerIfReplay(envelope?.data, { write: (chunk) => {
10588
11466
  process.stderr.write(chunk);
10589
11467
  } });
10590
- this.log(JSON.stringify(envelope?.data ?? null, null, 2));
11468
+ this.log(JSON.stringify(operationOutputPayload(envelope, parsedFlags.envelope === true), null, 2));
10591
11469
  });
10592
11470
  }
10593
11471
  }
@@ -11345,6 +12223,92 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
11345
12223
  }
11346
12224
  };
11347
12225
  //#endregion
12226
+ //#region src/oclif/function-deploy-wait.ts
12227
+ function validateDeployWaitFlags(params) {
12228
+ if (params.timeoutSeconds < 0) return "--timeout must be greater than or equal to 0.";
12229
+ if (params.pollIntervalSeconds <= 0) return "--poll-interval must be greater than 0.";
12230
+ return null;
12231
+ }
12232
+ function isTerminal(status) {
12233
+ return status === "deployed" || status === "failed";
12234
+ }
12235
+ function resultForTerminal(snapshot) {
12236
+ if (snapshot.deploy_status === "failed") return {
12237
+ function: snapshot,
12238
+ kind: "failed"
12239
+ };
12240
+ return {
12241
+ function: snapshot,
12242
+ kind: "ok"
12243
+ };
12244
+ }
12245
+ function toDeployWaitSnapshot(value) {
12246
+ return {
12247
+ ...value.created_at !== void 0 ? { created_at: value.created_at } : {},
12248
+ ...value.deploy_error !== void 0 ? { deploy_error: value.deploy_error } : {},
12249
+ deploy_status: value.deploy_status,
12250
+ ...value.deployed_at !== void 0 ? { deployed_at: value.deployed_at } : {},
12251
+ gateway_url: value.gateway_url,
12252
+ id: value.id,
12253
+ name: value.name,
12254
+ ...value.updated_at !== void 0 ? { updated_at: value.updated_at } : {}
12255
+ };
12256
+ }
12257
+ function elapsedSeconds(startedAt, now) {
12258
+ return Math.max(0, Math.round((now() - startedAt) / 1e3));
12259
+ }
12260
+ async function defaultSleep(ms) {
12261
+ await new Promise((resolve) => setTimeout(resolve, ms));
12262
+ }
12263
+ async function waitForFunctionDeploy(params) {
12264
+ const now = params.now ?? Date.now;
12265
+ const sleep = params.sleep ?? defaultSleep;
12266
+ const writeStderr = params.writeStderr ?? ((chunk) => {
12267
+ process.stderr.write(chunk);
12268
+ });
12269
+ const startedAt = now();
12270
+ const timeoutMs = params.timeoutSeconds * 1e3;
12271
+ const pollIntervalMs = params.pollIntervalSeconds * 1e3;
12272
+ const hasTimeout = params.timeoutSeconds > 0;
12273
+ let last = params.initial ? toDeployWaitSnapshot(params.initial) : null;
12274
+ let lastStatus = last?.deploy_status ?? "unknown";
12275
+ if (last && isTerminal(last.deploy_status)) return resultForTerminal(last);
12276
+ writeStderr(`Waiting for function ${params.id} deploy to finish (current status: ${lastStatus})...\n`);
12277
+ while (true) {
12278
+ const elapsedMs = now() - startedAt;
12279
+ if (hasTimeout && elapsedMs >= timeoutMs) return {
12280
+ elapsedSeconds: elapsedSeconds(startedAt, now),
12281
+ kind: "timeout",
12282
+ lastFunction: last
12283
+ };
12284
+ await sleep(hasTimeout ? Math.min(pollIntervalMs, Math.max(0, timeoutMs - elapsedMs)) : pollIntervalMs);
12285
+ if (hasTimeout && now() - startedAt >= timeoutMs) return {
12286
+ elapsedSeconds: elapsedSeconds(startedAt, now),
12287
+ kind: "timeout",
12288
+ lastFunction: last
12289
+ };
12290
+ const result = await params.getFunction({ id: params.id });
12291
+ if (result.error) return {
12292
+ kind: "error",
12293
+ payload: extractErrorPayload(result.error)
12294
+ };
12295
+ const fetched = result.data?.data;
12296
+ if (!fetched) return {
12297
+ kind: "error",
12298
+ payload: {
12299
+ code: "client_error",
12300
+ message: "Get function returned no data while waiting for deploy"
12301
+ }
12302
+ };
12303
+ last = toDeployWaitSnapshot(fetched);
12304
+ if (last.deploy_status !== lastStatus) {
12305
+ lastStatus = last.deploy_status;
12306
+ writeStderr(`Function ${params.id} deploy status: ${last.deploy_status}\n`);
12307
+ }
12308
+ if (isTerminal(last.deploy_status)) return resultForTerminal(last);
12309
+ }
12310
+ }
12311
+ //#endregion
11348
12312
  //#region src/oclif/lint/raw-send-mail-fetch.ts
11349
12313
  const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
11350
12314
  const SNIPPET_PADDING = 60;
@@ -11382,41 +12346,321 @@ function emitRawSendMailFetchWarning(bundleText, write) {
11382
12346
  //#endregion
11383
12347
  //#region src/oclif/secret-flags.ts
11384
12348
  const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
11385
- function parseSecretFlags(raw) {
12349
+ function resolveSecretFlags(input) {
11386
12350
  const secrets = [];
11387
12351
  const seenKeys = /* @__PURE__ */ new Set();
11388
- for (const entry of raw) {
11389
- const eq = entry.indexOf("=");
11390
- if (eq === -1) return {
12352
+ const env = input.env ?? process.env;
12353
+ const readFile = input.readFile ?? defaultReadFile$1;
12354
+ const readStdin = input.readStdin ?? defaultReadStdin$1;
12355
+ const envFileCache = /* @__PURE__ */ new Map();
12356
+ const reserveSecretKey = (key, sourceLabel) => {
12357
+ const keyError = validateKey(key, sourceLabel);
12358
+ if (keyError) return keyError;
12359
+ if (seenKeys.has(key)) return duplicateKeyError(key);
12360
+ seenKeys.add(key);
12361
+ return null;
12362
+ };
12363
+ const addSecret = (key, value, sourceLabel) => {
12364
+ const keyError = reserveSecretKey(key, sourceLabel);
12365
+ if (keyError) return keyError;
12366
+ secrets.push({
12367
+ key,
12368
+ value
12369
+ });
12370
+ return null;
12371
+ };
12372
+ for (const entry of input.inline ?? []) {
12373
+ const parsed = parseKeyValueFlag(entry, "--secret");
12374
+ if (parsed.kind === "error") return parsed;
12375
+ const error = addSecret(parsed.key, parsed.value, "--secret");
12376
+ if (error) return error;
12377
+ }
12378
+ for (const key of input.fromEnv ?? []) {
12379
+ const keyError = reserveSecretKey(key, "--secret-from-env");
12380
+ if (keyError) return keyError;
12381
+ const value = env[key];
12382
+ if (value === void 0) return {
11391
12383
  kind: "error",
11392
- message: `--secret expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`
12384
+ message: `--secret-from-env ${key} could not read ${key}: environment variable is not set.`
12385
+ };
12386
+ secrets.push({
12387
+ key,
12388
+ value
12389
+ });
12390
+ }
12391
+ for (const entry of input.fromFile ?? []) {
12392
+ const parsed = parseKeyValueFlag(entry, "--secret-from-file");
12393
+ if (parsed.kind === "error") return parsed;
12394
+ const keyError = reserveSecretKey(parsed.key, "--secret-from-file");
12395
+ if (keyError) return keyError;
12396
+ const file = readSecretFile(parsed.value, "--secret-from-file", readFile);
12397
+ if (file.kind === "error") return file;
12398
+ secrets.push({
12399
+ key: parsed.key,
12400
+ value: file.value
12401
+ });
12402
+ }
12403
+ for (const entry of input.fromEnvFile ?? []) {
12404
+ const parsed = parseEnvFileKeyRef(entry, "--secret-from-env-file");
12405
+ if (parsed.kind === "error") return parsed;
12406
+ const keyError = reserveSecretKey(parsed.key, "--secret-from-env-file");
12407
+ if (keyError) return keyError;
12408
+ const file = readEnvFile(parsed.path, readFile, envFileCache);
12409
+ if (file.kind === "error") return file;
12410
+ const value = file.values.get(parsed.key);
12411
+ if (value === void 0) return {
12412
+ kind: "error",
12413
+ message: `--secret-from-env-file ${entry} could not read ${parsed.key}: key is not present in ${parsed.path}.`
12414
+ };
12415
+ secrets.push({
12416
+ key: parsed.key,
12417
+ value
12418
+ });
12419
+ }
12420
+ if (input.fromStdin !== void 0) {
12421
+ const keyError = reserveSecretKey(input.fromStdin, "--secret-from-stdin");
12422
+ if (keyError) return keyError;
12423
+ const stdin = readSecretStdin("--secret-from-stdin", readStdin);
12424
+ if (stdin.kind === "error") return stdin;
12425
+ secrets.push({
12426
+ key: input.fromStdin,
12427
+ value: stdin.value
12428
+ });
12429
+ }
12430
+ return {
12431
+ kind: "ok",
12432
+ secrets
12433
+ };
12434
+ }
12435
+ function resolveSingleSecretValue(input) {
12436
+ const keyError = validateKey(input.key, "--key");
12437
+ if (keyError) return keyError;
12438
+ if ([
12439
+ input.value !== void 0 ? "--value" : null,
12440
+ input.valueFromEnv !== void 0 ? "--value-from-env" : null,
12441
+ input.valueFile !== void 0 ? "--value-file" : null,
12442
+ input.valueFromEnvFile !== void 0 ? "--value-from-env-file" : null,
12443
+ input.stdin === true ? "--stdin" : null
12444
+ ].filter((v) => v !== null).length !== 1) return {
12445
+ kind: "error",
12446
+ message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
12447
+ };
12448
+ const env = input.env ?? process.env;
12449
+ const readFile = input.readFile ?? defaultReadFile$1;
12450
+ const readStdin = input.readStdin ?? defaultReadStdin$1;
12451
+ if (input.value !== void 0) return {
12452
+ kind: "ok",
12453
+ value: input.value
12454
+ };
12455
+ if (input.valueFromEnv !== void 0) {
12456
+ const value = env[input.valueFromEnv];
12457
+ if (value === void 0) return {
12458
+ kind: "error",
12459
+ message: `--value-from-env ${input.valueFromEnv} could not read ${input.valueFromEnv}: environment variable is not set.`
12460
+ };
12461
+ return {
12462
+ kind: "ok",
12463
+ value
12464
+ };
12465
+ }
12466
+ if (input.valueFile !== void 0) return readSecretFile(input.valueFile, "--value-file", readFile);
12467
+ if (input.valueFromEnvFile !== void 0) {
12468
+ const parsed = parseSingleValueEnvFileRef(input.valueFromEnvFile, input.key, "--value-from-env-file");
12469
+ if (parsed.kind === "error") return parsed;
12470
+ const file = readEnvFile(parsed.path, readFile, /* @__PURE__ */ new Map());
12471
+ if (file.kind === "error") return file;
12472
+ const value = file.values.get(parsed.key);
12473
+ if (value === void 0) return {
12474
+ kind: "error",
12475
+ message: `--value-from-env-file ${input.valueFromEnvFile} could not read ${parsed.key}: key is not present in ${parsed.path}.`
12476
+ };
12477
+ return {
12478
+ kind: "ok",
12479
+ value
12480
+ };
12481
+ }
12482
+ if (input.stdin === true) return readSecretStdin("--stdin", readStdin);
12483
+ return {
12484
+ kind: "error",
12485
+ message: "Pass exactly one of --value, --value-from-env, --value-file, --value-from-env-file, or --stdin."
12486
+ };
12487
+ }
12488
+ function defaultReadFile$1(path) {
12489
+ return readFileSync(path, "utf8");
12490
+ }
12491
+ function defaultReadStdin$1() {
12492
+ if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/env source instead.");
12493
+ return readFileSync(0, "utf8");
12494
+ }
12495
+ function parseKeyValueFlag(entry, flagLabel) {
12496
+ const eq = entry.indexOf("=");
12497
+ if (eq === -1) return {
12498
+ kind: "error",
12499
+ message: `${flagLabel} expects KEY=VALUE (got ${JSON.stringify(entry)}). Example: ${flagLabel} API_TOKEN=abc123`
12500
+ };
12501
+ const key = entry.slice(0, eq);
12502
+ const value = entry.slice(eq + 1);
12503
+ if (key.length === 0) return {
12504
+ kind: "error",
12505
+ message: `${flagLabel} is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: ${flagLabel} API_TOKEN=abc123`
12506
+ };
12507
+ return {
12508
+ kind: "ok",
12509
+ key,
12510
+ value
12511
+ };
12512
+ }
12513
+ function validateKey(key, flagLabel) {
12514
+ if (!SECRET_KEY_RE.test(key)) return {
12515
+ kind: "error",
12516
+ message: `${flagLabel} KEY ${JSON.stringify(key)} does not match ${SECRET_KEY_RE.source} (uppercase letters, digits, underscores; first character is a letter or underscore).`
12517
+ };
12518
+ return null;
12519
+ }
12520
+ function duplicateKeyError(key) {
12521
+ return {
12522
+ kind: "error",
12523
+ message: `Secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`
12524
+ };
12525
+ }
12526
+ function readSecretFile(path, flagLabel, readFile) {
12527
+ try {
12528
+ return {
12529
+ kind: "ok",
12530
+ value: readFile(path)
11393
12531
  };
11394
- const key = entry.slice(0, eq);
11395
- const value = entry.slice(eq + 1);
11396
- if (key.length === 0) return {
12532
+ } catch (error) {
12533
+ return {
11397
12534
  kind: "error",
11398
- message: `--secret is missing a KEY before '=' (got ${JSON.stringify(entry)}). Example: --secret API_TOKEN=abc123`
12535
+ message: `Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`
11399
12536
  };
11400
- if (!SECRET_KEY_RE.test(key)) return {
12537
+ }
12538
+ }
12539
+ function readSecretStdin(flagLabel, readStdin) {
12540
+ try {
12541
+ return {
12542
+ kind: "ok",
12543
+ value: stripOneTrailingLineEnding(readStdin())
12544
+ };
12545
+ } catch (error) {
12546
+ return {
11401
12547
  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).`
12548
+ message: `Could not read ${flagLabel}: ${error instanceof Error ? error.message : String(error)}`
11403
12549
  };
11404
- if (seenKeys.has(key)) return {
12550
+ }
12551
+ }
12552
+ function stripOneTrailingLineEnding(value) {
12553
+ if (!value.endsWith("\n")) return value;
12554
+ const withoutLf = value.slice(0, -1);
12555
+ return withoutLf.endsWith("\r") ? withoutLf.slice(0, -1) : withoutLf;
12556
+ }
12557
+ function parseEnvFileKeyRef(entry, flagLabel) {
12558
+ const sep = entry.lastIndexOf(":");
12559
+ if (sep <= 0 || sep === entry.length - 1) return {
12560
+ kind: "error",
12561
+ message: `${flagLabel} expects FILE:KEY (got ${JSON.stringify(entry)}). Example: ${flagLabel} .env.local:OPENAI_KEY`
12562
+ };
12563
+ const path = entry.slice(0, sep);
12564
+ const key = entry.slice(sep + 1);
12565
+ const keyError = validateKey(key, flagLabel);
12566
+ if (keyError) return keyError;
12567
+ return {
12568
+ kind: "ok",
12569
+ key,
12570
+ path
12571
+ };
12572
+ }
12573
+ function parseSingleValueEnvFileRef(entry, fallbackKey, flagLabel) {
12574
+ const sep = entry.lastIndexOf(":");
12575
+ if (sep === -1) return {
12576
+ kind: "ok",
12577
+ key: fallbackKey,
12578
+ path: entry
12579
+ };
12580
+ if (sep <= 0 || sep === entry.length - 1) return {
12581
+ kind: "error",
12582
+ message: `${flagLabel} expects FILE or FILE:KEY (got ${JSON.stringify(entry)}). Example: ${flagLabel} .env.local or ${flagLabel} .env.local:OPENAI_KEY`
12583
+ };
12584
+ const path = entry.slice(0, sep);
12585
+ const key = entry.slice(sep + 1);
12586
+ const keyError = validateKey(key, flagLabel);
12587
+ if (keyError) return keyError;
12588
+ return {
12589
+ kind: "ok",
12590
+ key,
12591
+ path
12592
+ };
12593
+ }
12594
+ function readEnvFile(path, readFile, cache) {
12595
+ const cached = cache.get(path);
12596
+ if (cached) return {
12597
+ kind: "ok",
12598
+ values: cached
12599
+ };
12600
+ let contents;
12601
+ try {
12602
+ contents = readFile(path);
12603
+ } catch (error) {
12604
+ return {
11405
12605
  kind: "error",
11406
- message: `--secret KEY ${JSON.stringify(key)} was passed more than once. Each key may only appear once per command.`
12606
+ message: `Could not read env file ${path}: ${error instanceof Error ? error.message : String(error)}`
11407
12607
  };
11408
- seenKeys.add(key);
11409
- secrets.push({
11410
- key,
11411
- value
11412
- });
11413
12608
  }
12609
+ const values = parseEnvFile(contents);
12610
+ cache.set(path, values);
11414
12611
  return {
11415
12612
  kind: "ok",
11416
- secrets
12613
+ values
11417
12614
  };
11418
12615
  }
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.).";
12616
+ function parseEnvFile(contents) {
12617
+ const values = /* @__PURE__ */ new Map();
12618
+ const normalized = contents.replace(/^\uFEFF/, "");
12619
+ for (const rawLine of normalized.split(/\r?\n/)) {
12620
+ let line = rawLine.trimStart();
12621
+ if (line.length === 0 || line.startsWith("#")) continue;
12622
+ if (line.startsWith("export ")) line = line.slice(7).trimStart();
12623
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
12624
+ if (!match) continue;
12625
+ values.set(match[1], parseEnvValue(match[2] ?? ""));
12626
+ }
12627
+ return values;
12628
+ }
12629
+ function parseEnvValue(raw) {
12630
+ const value = raw.trimStart();
12631
+ if (value.startsWith("'")) {
12632
+ const end = value.indexOf("'", 1);
12633
+ return end === -1 ? value.slice(1) : value.slice(1, end);
12634
+ }
12635
+ if (value.startsWith("\"")) return parseDoubleQuotedEnvValue(value);
12636
+ return value.replace(/\s+#.*$/, "").trimEnd();
12637
+ }
12638
+ function parseDoubleQuotedEnvValue(value) {
12639
+ let out = "";
12640
+ let escaped = false;
12641
+ for (let i = 1; i < value.length; i++) {
12642
+ const ch = value[i];
12643
+ if (escaped) {
12644
+ if (ch === "n") out += "\n";
12645
+ else if (ch === "r") out += "\r";
12646
+ else if (ch === "t") out += " ";
12647
+ else out += ch;
12648
+ escaped = false;
12649
+ continue;
12650
+ }
12651
+ if (ch === "\\") {
12652
+ escaped = true;
12653
+ continue;
12654
+ }
12655
+ if (ch === "\"") break;
12656
+ out += ch;
12657
+ }
12658
+ if (escaped) out += "\\";
12659
+ return out;
12660
+ }
12661
+ 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.";
12662
+ 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.";
12663
+ 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
12664
  //#endregion
11421
12665
  //#region src/oclif/commands/functions-deploy.ts
11422
12666
  async function runDeployWithSecrets(api, params) {
@@ -11518,22 +12762,24 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11518
12762
  \`functions:create-function\` if you need the full flag surface
11519
12763
  (raw-body JSON, etc.).
11520
12764
 
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.`;
12765
+ Pass secret source flags to seed bindings in the same command. Keys
12766
+ must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
12767
+ underscores; first character is a letter or underscore). With one
12768
+ or more secrets the deploy fans out to multiple API calls:
12769
+ create-function, set-secret per pair, then a final update-function
12770
+ with the same bundle so the running handler picks up the bindings.
12771
+ If a secret write fails after the create step the function exists
12772
+ with whatever secrets succeeded and the redeploy has NOT fired;
12773
+ re-run \`primitive functions set-secret\` for the missing keys, then
12774
+ \`primitive functions redeploy\` to push them live. ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
11532
12775
  static summary = "Deploy a new function from a bundled handler file";
11533
12776
  static examples = [
11534
12777
  "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js",
12778
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --wait",
11535
12779
  "<%= 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"
12780
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
12781
+ "<%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-env-file .env.local:OWNER_EMAIL",
12782
+ "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions deploy --name forwarder --file ./bundle.js --secret-from-stdin OPENAI_KEY"
11537
12783
  ];
11538
12784
  static flags = {
11539
12785
  "api-key": Flags.string({
@@ -11563,12 +12809,49 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11563
12809
  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
12810
  multiple: true
11565
12811
  }),
12812
+ "secret-from-env": Flags.string({
12813
+ 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.",
12814
+ multiple: true
12815
+ }),
12816
+ "secret-from-file": Flags.string({
12817
+ 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.",
12818
+ multiple: true
12819
+ }),
12820
+ "secret-from-env-file": Flags.string({
12821
+ 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.",
12822
+ multiple: true
12823
+ }),
12824
+ "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." }),
12825
+ wait: Flags.boolean({ description: "Wait until the function deploy reaches deployed or failed. Progress is written to stderr; stdout remains the final JSON payload." }),
12826
+ timeout: Flags.integer({
12827
+ default: 120,
12828
+ description: "Seconds to wait when --wait is set before exiting non-zero. Use 0 to wait forever."
12829
+ }),
12830
+ "poll-interval": Flags.integer({
12831
+ default: 2,
12832
+ description: "Seconds between deploy-status polls when --wait is set."
12833
+ }),
11566
12834
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11567
12835
  };
11568
12836
  async run() {
11569
12837
  const { flags } = await this.parse(FunctionsDeployCommand);
11570
12838
  await runWithTiming(flags.time, async () => {
11571
- const parsedSecrets = parseSecretFlags(flags.secret ?? []);
12839
+ const waitFlagError = validateDeployWaitFlags({
12840
+ pollIntervalSeconds: flags["poll-interval"],
12841
+ timeoutSeconds: flags.timeout
12842
+ });
12843
+ if (waitFlagError) {
12844
+ process.stderr.write(`${waitFlagError}\n`);
12845
+ process.exitCode = 1;
12846
+ return;
12847
+ }
12848
+ const parsedSecrets = resolveSecretFlags({
12849
+ fromEnv: flags["secret-from-env"] ?? [],
12850
+ fromEnvFile: flags["secret-from-env-file"] ?? [],
12851
+ fromFile: flags["secret-from-file"] ?? [],
12852
+ fromStdin: flags["secret-from-stdin"],
12853
+ inline: flags.secret ?? []
12854
+ });
11572
12855
  if (parsedSecrets.kind === "error") {
11573
12856
  process.stderr.write(`${parsedSecrets.message}\n`);
11574
12857
  process.exitCode = 1;
@@ -11647,19 +12930,57 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
11647
12930
  return;
11648
12931
  }
11649
12932
  const payload = outcome.result.redeploy ?? outcome.result.created;
12933
+ if (flags.wait) {
12934
+ const waitResult = await waitForFunctionDeploy({
12935
+ getFunction: (p) => getFunction({
12936
+ client: apiClient.client,
12937
+ path: { id: p.id },
12938
+ responseStyle: "fields"
12939
+ }),
12940
+ id: payload.id,
12941
+ initial: payload,
12942
+ pollIntervalSeconds: flags["poll-interval"],
12943
+ timeoutSeconds: flags.timeout,
12944
+ writeStderr: (chunk) => process.stderr.write(chunk)
12945
+ });
12946
+ if (waitResult.kind === "error") {
12947
+ writeErrorWithHints(waitResult.payload);
12948
+ removeStaleSavedCredentialOnUnauthorized({
12949
+ ...authFailureContext,
12950
+ payload: waitResult.payload
12951
+ });
12952
+ process.exitCode = 1;
12953
+ return;
12954
+ }
12955
+ if (waitResult.kind === "timeout") {
12956
+ const status = waitResult.lastFunction?.deploy_status ?? "unknown";
12957
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for function ${payload.id} deploy to finish (last status: ${status}).\n`);
12958
+ process.exitCode = 2;
12959
+ return;
12960
+ }
12961
+ this.log(JSON.stringify(waitResult.function, null, 2));
12962
+ if (waitResult.kind === "failed") {
12963
+ const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
12964
+ process.stderr.write(`Function ${payload.id} deploy failed${detail}\n`);
12965
+ process.exitCode = 1;
12966
+ }
12967
+ return;
12968
+ }
11650
12969
  this.log(JSON.stringify(payload, null, 2));
11651
12970
  });
11652
12971
  }
11653
12972
  };
11654
12973
  //#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";
12974
+ //#region src/oclif/function-templates.ts
12975
+ const DEFAULT_FUNCTION_TEMPLATE_ID = "email-reply";
12976
+ const PRIMITIVE_TEAM_AUTHOR = {
12977
+ id: "primitive-team",
12978
+ name: "Primitive Team",
12979
+ url: "https://primitive.dev"
12980
+ };
12981
+ const SDK_VERSION_RANGE = "^0.30.0";
12982
+ const CLI_VERSION_RANGE = "^0.30.0";
11658
12983
  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
12984
  function renderHandler() {
11664
12985
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
11665
12986
  //
@@ -11880,7 +13201,7 @@ Run \`primitive login\` once to save a key in your CLI config if you
11880
13201
  prefer that to an env var.
11881
13202
  `;
11882
13203
  }
11883
- function scaffoldFiles(name) {
13204
+ function renderEmailReplyTemplateFiles(name) {
11884
13205
  return [
11885
13206
  {
11886
13207
  contents: renderHandler(),
@@ -11908,9 +13229,79 @@ function scaffoldFiles(name) {
11908
13229
  }
11909
13230
  ];
11910
13231
  }
13232
+ const FUNCTION_TEMPLATES = [{
13233
+ author: PRIMITIVE_TEAM_AUTHOR,
13234
+ dependencies: ["@primitivedotdev/sdk"],
13235
+ description: "A deployable TypeScript email handler that validates email.received events, skips likely loops, and replies with the Primitive SDK.",
13236
+ devDependencies: [
13237
+ "@primitivedotdev/cli",
13238
+ "esbuild",
13239
+ "typescript"
13240
+ ],
13241
+ files: ({ name }) => renderEmailReplyTemplateFiles(name),
13242
+ id: DEFAULT_FUNCTION_TEMPLATE_ID,
13243
+ secrets: [],
13244
+ summary: "Reply to inbound email with the Primitive SDK.",
13245
+ tags: [
13246
+ "email",
13247
+ "reply",
13248
+ "typescript",
13249
+ "worker"
13250
+ ],
13251
+ title: "Email Reply"
13252
+ }];
13253
+ function functionTemplateIds(templates) {
13254
+ return templates.map((template) => template.id);
13255
+ }
13256
+ function findFunctionTemplate(templates, id) {
13257
+ return templates.find((template) => template.id === id) ?? null;
13258
+ }
13259
+ function serializeFunctionTemplate(template) {
13260
+ return {
13261
+ author: { ...template.author },
13262
+ dependencies: [...template.dependencies],
13263
+ description: template.description,
13264
+ devDependencies: [...template.devDependencies],
13265
+ id: template.id,
13266
+ secrets: [...template.secrets],
13267
+ summary: template.summary,
13268
+ tags: [...template.tags],
13269
+ title: template.title
13270
+ };
13271
+ }
13272
+ function formatFunctionTemplateList(templates) {
13273
+ const lines = ["Available Primitive Function templates:", ""];
13274
+ for (const template of templates) {
13275
+ lines.push(`${template.id}`);
13276
+ lines.push(` title: ${template.title}`);
13277
+ lines.push(` author: ${template.author.name}`);
13278
+ lines.push(` summary: ${template.summary}`);
13279
+ lines.push(` tags: ${template.tags.length > 0 ? template.tags.join(", ") : "none"}`);
13280
+ lines.push(` secrets: ${template.secrets.length > 0 ? template.secrets.join(", ") : "none"}`);
13281
+ lines.push("");
13282
+ }
13283
+ lines.push("Use `primitive functions init <name> --template <id>`.");
13284
+ return lines.join("\n");
13285
+ }
13286
+ //#endregion
13287
+ //#region src/oclif/commands/functions-init.ts
13288
+ const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
13289
+ function isValidFunctionName(name) {
13290
+ return VALID_NAME.test(name);
13291
+ }
13292
+ function unknownTemplateError(templateId) {
13293
+ const available = functionTemplateIds(FUNCTION_TEMPLATES).join(", ");
13294
+ return new Errors.CLIError(`Unknown function template "${templateId}". Available templates: ${available}. Run \`primitive functions templates\` for details.`, { exit: 1 });
13295
+ }
13296
+ function scaffoldFiles(name, templateId = DEFAULT_FUNCTION_TEMPLATE_ID) {
13297
+ const template = findFunctionTemplate(FUNCTION_TEMPLATES, templateId);
13298
+ if (!template) throw unknownTemplateError(templateId);
13299
+ return template.files({ name });
13300
+ }
11911
13301
  function writeScaffold(params) {
11912
13302
  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);
13303
+ const templateId = params.templateId ?? "email-reply";
13304
+ const files = scaffoldFiles(params.name, templateId);
11914
13305
  const written = [];
11915
13306
  try {
11916
13307
  mkdirSync(params.outDir, { recursive: false });
@@ -11941,7 +13332,7 @@ function writeScaffold(params) {
11941
13332
  return { written };
11942
13333
  }
11943
13334
  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.
13335
+ static description = `Scaffold a new Primitive Function project from a Primitive-owned template.
11945
13336
 
11946
13337
  The scaffolded handler imports \`createPrimitiveClient\` from
11947
13338
  \`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
@@ -11950,22 +13341,35 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11950
13341
  ./dist/handler.js, ready to hand to \`primitive functions deploy --file\`.
11951
13342
 
11952
13343
  Refuses to overwrite an existing directory. Use --out-dir to pick a
11953
- different target path than ./<name>/.`;
13344
+ different target path than ./<name>/. Run \`primitive functions templates\`
13345
+ to inspect available templates.`;
11954
13346
  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"];
13347
+ static examples = [
13348
+ "<%= config.bin %> functions init my-fn",
13349
+ "<%= config.bin %> functions init my-fn --template email-reply",
13350
+ "<%= config.bin %> functions init my-fn --out-dir ./functions/my-fn"
13351
+ ];
11956
13352
  static args = { name: Args.string({
11957
13353
  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
13354
  required: true
11959
13355
  }) };
11960
- static flags = { "out-dir": Flags.string({ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist." }) };
13356
+ static flags = {
13357
+ "out-dir": Flags.string({ description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist." }),
13358
+ template: Flags.string({
13359
+ default: DEFAULT_FUNCTION_TEMPLATE_ID,
13360
+ description: "Function template id. Run `primitive functions templates` to list templates.",
13361
+ options: functionTemplateIds(FUNCTION_TEMPLATES)
13362
+ })
13363
+ };
11961
13364
  async run() {
11962
13365
  const { args, flags } = await this.parse(FunctionsInitCommand);
11963
13366
  const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
11964
13367
  writeScaffold({
11965
13368
  name: args.name,
11966
- outDir
13369
+ outDir,
13370
+ templateId: flags.template
11967
13371
  });
11968
- this.log(`Scaffolded ${outDir}.`);
13372
+ this.log(`Scaffolded ${outDir} from ${flags.template} template.`);
11969
13373
  this.log("Next:");
11970
13374
  this.log(` cd ${outDir}`);
11971
13375
  this.log(" npm install");
@@ -11974,6 +13378,166 @@ var FunctionsInitCommand = class FunctionsInitCommand extends Command {
11974
13378
  }
11975
13379
  };
11976
13380
  //#endregion
13381
+ //#region src/oclif/commands/functions-logs.ts
13382
+ const DEFAULT_LOG_LIMIT = 50;
13383
+ const DEFAULT_LOG_POLL_INTERVAL_SECONDS = 2;
13384
+ function levelLabel(level) {
13385
+ return level.toUpperCase().padEnd(5);
13386
+ }
13387
+ function orderFunctionLogsForDisplay(rows) {
13388
+ return [...rows].reverse();
13389
+ }
13390
+ function formatFunctionLogLine(row) {
13391
+ const metadata = row.metadata && Object.keys(row.metadata).length > 0 ? ` ${JSON.stringify(row.metadata)}` : "";
13392
+ return `${row.ts} ${levelLabel(row.level)} ${row.message}${metadata}`;
13393
+ }
13394
+ function collectFreshFunctionLogsFromPage(rows, seenIds) {
13395
+ const freshNewestFirst = [];
13396
+ let reachedSeen = false;
13397
+ for (const row of rows) {
13398
+ if (seenIds.has(row.id)) {
13399
+ reachedSeen = true;
13400
+ continue;
13401
+ }
13402
+ freshNewestFirst.push(row);
13403
+ seenIds.add(row.id);
13404
+ }
13405
+ return {
13406
+ freshNewestFirst,
13407
+ reachedSeen
13408
+ };
13409
+ }
13410
+ function emitLogRows(rows, jsonl) {
13411
+ for (const row of rows) {
13412
+ const line = jsonl ? JSON.stringify(row) : formatFunctionLogLine(row);
13413
+ process.stdout.write(`${line}\n`);
13414
+ }
13415
+ }
13416
+ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
13417
+ static description = "List or follow function execution logs. Defaults to compact text output; use --jsonl for one JSON object per log row.";
13418
+ static summary = "List or follow a function's execution logs";
13419
+ static examples = [
13420
+ "<%= config.bin %> functions logs --id <fn-id>",
13421
+ "<%= config.bin %> functions logs --id <fn-id> --jsonl",
13422
+ "<%= config.bin %> functions logs --id <fn-id> --follow"
13423
+ ];
13424
+ static flags = {
13425
+ "api-key": Flags.string({
13426
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
13427
+ env: "PRIMITIVE_API_KEY"
13428
+ }),
13429
+ "api-base-url-1": Flags.string({
13430
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
13431
+ env: "PRIMITIVE_API_BASE_URL_1",
13432
+ hidden: true
13433
+ }),
13434
+ "api-base-url-2": Flags.string({
13435
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
13436
+ env: "PRIMITIVE_API_BASE_URL_2",
13437
+ hidden: true
13438
+ }),
13439
+ id: Flags.string({
13440
+ description: "Function id (UUID).",
13441
+ required: true
13442
+ }),
13443
+ limit: Flags.integer({
13444
+ default: DEFAULT_LOG_LIMIT,
13445
+ description: "Maximum rows to fetch per poll. Server clamps to 1..200."
13446
+ }),
13447
+ cursor: Flags.string({ description: "Opaque pagination cursor from a previous logs response. Not supported with --follow." }),
13448
+ follow: Flags.boolean({
13449
+ char: "f",
13450
+ description: "Keep polling the newest logs and print rows not seen yet."
13451
+ }),
13452
+ jsonl: Flags.boolean({ description: "Print one compact JSON object per log row." }),
13453
+ "poll-interval": Flags.integer({
13454
+ default: DEFAULT_LOG_POLL_INTERVAL_SECONDS,
13455
+ description: "Seconds between polls when --follow is set."
13456
+ }),
13457
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
13458
+ };
13459
+ async run() {
13460
+ const { flags } = await this.parse(FunctionsLogsCommand);
13461
+ if (flags.limit <= 0) this.error("--limit must be greater than 0.", { exit: 2 });
13462
+ if (flags["poll-interval"] <= 0) this.error("--poll-interval must be greater than 0.", { exit: 2 });
13463
+ if (flags.follow && flags.cursor) this.error("--cursor cannot be combined with --follow.", { exit: 2 });
13464
+ await runWithTiming(flags.time, async () => {
13465
+ const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
13466
+ const auth = resolveCliAuth({
13467
+ apiKey: flags["api-key"],
13468
+ apiBaseUrl1: flags["api-base-url-1"],
13469
+ apiBaseUrl2: flags["api-base-url-2"],
13470
+ configDir: this.config.configDir
13471
+ });
13472
+ const apiClient = new PrimitiveApiClient({
13473
+ apiKey: auth.apiKey,
13474
+ apiBaseUrl1: auth.apiBaseUrl1,
13475
+ apiBaseUrl2: auth.apiBaseUrl2
13476
+ });
13477
+ const seenIds = /* @__PURE__ */ new Set();
13478
+ let completedInitialFollowPoll = false;
13479
+ let hasObservedLogs = false;
13480
+ let wroteEmptyHint = false;
13481
+ while (true) {
13482
+ let cursor = flags.cursor;
13483
+ let nextCursor = null;
13484
+ let rows = [];
13485
+ while (true) {
13486
+ const result = await listFunctionLogs({
13487
+ client: apiClient.client,
13488
+ path: { id: flags.id },
13489
+ query: {
13490
+ ...cursor ? { cursor } : {},
13491
+ limit: flags.limit
13492
+ },
13493
+ responseStyle: "fields"
13494
+ });
13495
+ if (result.error) {
13496
+ const errorPayload = extractErrorPayload(result.error);
13497
+ writeErrorWithHints(errorPayload);
13498
+ removeStaleSavedCredentialOnUnauthorized({
13499
+ auth,
13500
+ baseUrlOverridden,
13501
+ configDir: this.config.configDir,
13502
+ payload: errorPayload
13503
+ });
13504
+ process.exitCode = 1;
13505
+ return;
13506
+ }
13507
+ const page = result.data?.data ?? {
13508
+ items: [],
13509
+ next_cursor: null
13510
+ };
13511
+ nextCursor = page.next_cursor;
13512
+ if (!flags.follow) {
13513
+ rows = orderFunctionLogsForDisplay(page.items);
13514
+ break;
13515
+ }
13516
+ if (page.items.length > 0) hasObservedLogs = true;
13517
+ const collected = collectFreshFunctionLogsFromPage(page.items, seenIds);
13518
+ rows.push(...collected.freshNewestFirst);
13519
+ if (!completedInitialFollowPoll || collected.reachedSeen || !page.next_cursor) {
13520
+ rows = orderFunctionLogsForDisplay(rows);
13521
+ break;
13522
+ }
13523
+ cursor = page.next_cursor;
13524
+ }
13525
+ if (rows.length === 0 && !wroteEmptyHint) {
13526
+ process.stderr.write(flags.follow ? hasObservedLogs ? "Waiting for new function logs...\n" : "No function logs yet. Waiting for new rows...\n" : "No function logs yet. Trigger the function, then run this command again.\n");
13527
+ wroteEmptyHint = true;
13528
+ }
13529
+ emitLogRows(rows, flags.jsonl);
13530
+ if (!flags.follow) {
13531
+ if (nextCursor) process.stderr.write(`next cursor: ${nextCursor}\n`);
13532
+ return;
13533
+ }
13534
+ completedInitialFollowPoll = true;
13535
+ await sleep$1(flags["poll-interval"] * 1e3);
13536
+ }
13537
+ });
13538
+ }
13539
+ };
13540
+ //#endregion
11977
13541
  //#region src/oclif/commands/functions-redeploy.ts
11978
13542
  async function runRedeployWithSecrets(api, params) {
11979
13543
  const writtenSecrets = [];
@@ -12046,17 +13610,20 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
12046
13610
  the bindings table fresh on every call, so passing the existing
12047
13611
  bundle picks up any secret writes since the last deploy.
12048
13612
 
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).`;
13613
+ Pass secret source flags to write secrets BEFORE the redeploy fires;
13614
+ one update-function call then refreshes every new binding. Keys must
13615
+ match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits, underscores;
13616
+ first character is a letter or underscore). With one or more secrets
13617
+ the redeploy fans out to multiple API calls (set-secret per pair,
13618
+ then update-function). ${SECRET_SOURCE_FLAGS_DESCRIPTION}`;
12055
13619
  static summary = "Redeploy a function from a bundled handler file";
12056
13620
  static examples = [
12057
13621
  "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js",
13622
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --wait",
12058
13623
  "<%= 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"
13624
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret OPENAI_KEY=sk-... --secret OWNER_EMAIL=me@example.com",
13625
+ "<%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-env OPENAI_KEY --secret-from-file PRIVATE_KEY=./private-key.pem",
13626
+ "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions redeploy --id <fn-id> --file ./bundle.js --secret-from-stdin OPENAI_KEY"
12060
13627
  ];
12061
13628
  static flags = {
12062
13629
  "api-key": Flags.string({
@@ -12086,12 +13653,49 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
12086
13653
  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
13654
  multiple: true
12088
13655
  }),
13656
+ "secret-from-env": Flags.string({
13657
+ 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.",
13658
+ multiple: true
13659
+ }),
13660
+ "secret-from-file": Flags.string({
13661
+ description: "Secret KEY=PATH to read from a UTF-8 file and write before the redeploy. Repeatable. The full file contents become the value.",
13662
+ multiple: true
13663
+ }),
13664
+ "secret-from-env-file": Flags.string({
13665
+ 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.",
13666
+ multiple: true
13667
+ }),
13668
+ "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." }),
13669
+ wait: Flags.boolean({ description: "Wait until the function deploy reaches deployed or failed. Progress is written to stderr; stdout remains the final JSON payload." }),
13670
+ timeout: Flags.integer({
13671
+ default: 120,
13672
+ description: "Seconds to wait when --wait is set before exiting non-zero. Use 0 to wait forever."
13673
+ }),
13674
+ "poll-interval": Flags.integer({
13675
+ default: 2,
13676
+ description: "Seconds between deploy-status polls when --wait is set."
13677
+ }),
12089
13678
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
12090
13679
  };
12091
13680
  async run() {
12092
13681
  const { flags } = await this.parse(FunctionsRedeployCommand);
12093
13682
  await runWithTiming(flags.time, async () => {
12094
- const parsedSecrets = parseSecretFlags(flags.secret ?? []);
13683
+ const waitFlagError = validateDeployWaitFlags({
13684
+ pollIntervalSeconds: flags["poll-interval"],
13685
+ timeoutSeconds: flags.timeout
13686
+ });
13687
+ if (waitFlagError) {
13688
+ process.stderr.write(`${waitFlagError}\n`);
13689
+ process.exitCode = 1;
13690
+ return;
13691
+ }
13692
+ const parsedSecrets = resolveSecretFlags({
13693
+ fromEnv: flags["secret-from-env"] ?? [],
13694
+ fromEnvFile: flags["secret-from-env-file"] ?? [],
13695
+ fromFile: flags["secret-from-file"] ?? [],
13696
+ fromStdin: flags["secret-from-stdin"],
13697
+ inline: flags.secret ?? []
13698
+ });
12095
13699
  if (parsedSecrets.kind === "error") {
12096
13700
  process.stderr.write(`${parsedSecrets.message}\n`);
12097
13701
  process.exitCode = 1;
@@ -12160,6 +13764,42 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
12160
13764
  process.exitCode = 1;
12161
13765
  return;
12162
13766
  }
13767
+ if (flags.wait) {
13768
+ const waitResult = await waitForFunctionDeploy({
13769
+ getFunction: (p) => getFunction({
13770
+ client: apiClient.client,
13771
+ path: { id: p.id },
13772
+ responseStyle: "fields"
13773
+ }),
13774
+ id: outcome.result.redeploy.id,
13775
+ initial: outcome.result.redeploy,
13776
+ pollIntervalSeconds: flags["poll-interval"],
13777
+ timeoutSeconds: flags.timeout,
13778
+ writeStderr: (chunk) => process.stderr.write(chunk)
13779
+ });
13780
+ if (waitResult.kind === "error") {
13781
+ writeErrorWithHints(waitResult.payload);
13782
+ removeStaleSavedCredentialOnUnauthorized({
13783
+ ...authFailureContext,
13784
+ payload: waitResult.payload
13785
+ });
13786
+ process.exitCode = 1;
13787
+ return;
13788
+ }
13789
+ if (waitResult.kind === "timeout") {
13790
+ const status = waitResult.lastFunction?.deploy_status ?? "unknown";
13791
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for function ${outcome.result.redeploy.id} deploy to finish (last status: ${status}).\n`);
13792
+ process.exitCode = 2;
13793
+ return;
13794
+ }
13795
+ this.log(JSON.stringify(waitResult.function, null, 2));
13796
+ if (waitResult.kind === "failed") {
13797
+ const detail = waitResult.function.deploy_error ? `: ${waitResult.function.deploy_error}` : ".";
13798
+ process.stderr.write(`Function ${outcome.result.redeploy.id} deploy failed${detail}\n`);
13799
+ process.exitCode = 1;
13800
+ }
13801
+ return;
13802
+ }
12163
13803
  this.log(JSON.stringify(outcome.result.redeploy, null, 2));
12164
13804
  });
12165
13805
  }
@@ -12242,9 +13882,15 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12242
13882
 
12243
13883
  Keys must match \`^[A-Z_][A-Z0-9_]*$\` (uppercase letters, digits,
12244
13884
  underscores; first character is a letter or underscore). System-
12245
- managed keys are reserved and rejected.`;
13885
+ managed keys are reserved and rejected. ${SINGLE_SECRET_VALUE_SOURCE_DESCRIPTION}`;
12246
13886
  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"];
13887
+ static examples = [
13888
+ "<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123",
13889
+ "<%= config.bin %> functions set-secret --id <fn-id> --key API_TOKEN --value abc123 --redeploy",
13890
+ "<%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env OPENAI_KEY --redeploy",
13891
+ "<%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --value-from-env-file .env.local --redeploy",
13892
+ "printf '%s' \"$OPENAI_KEY\" | <%= config.bin %> functions set-secret --id <fn-id> --key OPENAI_KEY --stdin --redeploy"
13893
+ ];
12248
13894
  static flags = {
12249
13895
  "api-key": Flags.string({
12250
13896
  description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
@@ -12268,10 +13914,11 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12268
13914
  description: "Secret key. Uppercase letters, digits, underscores; must start with a letter or underscore. System-managed keys are reserved.",
12269
13915
  required: true
12270
13916
  }),
12271
- value: Flags.string({
12272
- description: "Secret value (up to 4096 UTF-8 bytes). Encrypted at rest.",
12273
- required: true
12274
- }),
13917
+ 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." }),
13918
+ "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." }),
13919
+ "value-file": Flags.string({ description: "UTF-8 file to read as the secret value. The full file contents become the value." }),
13920
+ "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." }),
13921
+ 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
13922
  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
13923
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
12277
13924
  };
@@ -12295,6 +13942,19 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12295
13942
  baseUrlOverridden,
12296
13943
  configDir: this.config.configDir
12297
13944
  };
13945
+ const resolvedValue = resolveSingleSecretValue({
13946
+ key: flags.key,
13947
+ value: flags.value,
13948
+ valueFile: flags["value-file"],
13949
+ valueFromEnv: flags["value-from-env"],
13950
+ valueFromEnvFile: flags["value-from-env-file"],
13951
+ stdin: flags.stdin
13952
+ });
13953
+ if (resolvedValue.kind === "error") {
13954
+ process.stderr.write(`${resolvedValue.message}\n`);
13955
+ process.exitCode = 1;
13956
+ return;
13957
+ }
12298
13958
  const outcome = await runSetSecret({
12299
13959
  getFunction: (p) => getFunction({
12300
13960
  client: apiClient.client,
@@ -12320,7 +13980,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12320
13980
  id: flags.id,
12321
13981
  key: flags.key,
12322
13982
  redeploy: flags.redeploy === true,
12323
- value: flags.value
13983
+ value: resolvedValue.value
12324
13984
  });
12325
13985
  if (outcome.kind === "error") {
12326
13986
  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 +13998,116 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
12338
13998
  }
12339
13999
  };
12340
14000
  //#endregion
14001
+ //#region src/oclif/commands/functions-templates.ts
14002
+ var FunctionsTemplatesCommand = class FunctionsTemplatesCommand extends Command {
14003
+ static enableJsonFlag = true;
14004
+ static description = `List Primitive Function templates available to \`primitive functions init\`.
14005
+
14006
+ The default table is optimized for quick terminal discovery. Use
14007
+ --json when an agent or script needs stable metadata for searching,
14008
+ ranking, or choosing a template programmatically.`;
14009
+ static summary = "List available Primitive Function templates";
14010
+ static examples = [
14011
+ "<%= config.bin %> functions templates",
14012
+ "<%= config.bin %> functions templates --json",
14013
+ "<%= config.bin %> functions init my-fn --template email-reply"
14014
+ ];
14015
+ static flags = {};
14016
+ async run() {
14017
+ const { flags } = await this.parse(FunctionsTemplatesCommand);
14018
+ if (flags.json) return FUNCTION_TEMPLATES.map(serializeFunctionTemplate);
14019
+ this.log(formatFunctionTemplateList(FUNCTION_TEMPLATES));
14020
+ }
14021
+ };
14022
+ //#endregion
12341
14023
  //#region src/oclif/commands/functions-test-function.ts
12342
14024
  const DEFAULT_WAIT_TIMEOUT_SECONDS = 60;
12343
14025
  const TERMINAL_WEBHOOK_STATUSES = new Set(["fired", "exhausted"]);
14026
+ function buildFunctionTestOutcome(params) {
14027
+ const outcome = {
14028
+ elapsed_seconds: params.elapsedSeconds,
14029
+ function_id: params.functionId,
14030
+ inbound_domain: params.invocation.inbound_domain,
14031
+ inbound_id: params.inboundId,
14032
+ inbound_to: params.invocation.to,
14033
+ poll_since: params.invocation.poll_since,
14034
+ test_run_id: params.invocation.test_run_id,
14035
+ test_send_id: params.invocation.send_id,
14036
+ test_subject: params.invocation.subject,
14037
+ trace_url: params.invocation.trace_url,
14038
+ watch_url: params.invocation.watch_url,
14039
+ webhook_attempt_count: params.detail.webhook_attempt_count,
14040
+ webhook_last_error: params.detail.webhook_last_error,
14041
+ webhook_last_status_code: params.detail.webhook_last_status_code,
14042
+ webhook_status: params.detail.webhook_status
14043
+ };
14044
+ if (params.showSends) outcome.sent_emails = params.detail.replies;
14045
+ return outcome;
14046
+ }
14047
+ function writeFunctionTestProgress(message, writeStderr = (chunk) => {
14048
+ process.stderr.write(chunk);
14049
+ }) {
14050
+ writeStderr(`${message}\n`);
14051
+ }
14052
+ function stringOrNull(value) {
14053
+ return typeof value === "string" && value.length > 0 ? value : null;
14054
+ }
14055
+ function findMatchingFunctionEndpoints(params) {
14056
+ const matches = [];
14057
+ for (const endpoint of params.endpoints) {
14058
+ if (endpoint.kind !== "function") continue;
14059
+ if (endpoint.enabled === false) continue;
14060
+ if (endpoint.deactivated_at !== null && endpoint.deactivated_at !== void 0) continue;
14061
+ const id = stringOrNull(endpoint.id);
14062
+ if (!id) continue;
14063
+ const domainId = stringOrNull(endpoint.domain_id);
14064
+ if (domainId !== null && (params.inboundDomainId === null || domainId !== params.inboundDomainId)) continue;
14065
+ const functionId = stringOrNull(endpoint.function_id);
14066
+ matches.push({
14067
+ function_id: functionId,
14068
+ id,
14069
+ is_current_function: functionId === params.currentFunctionId,
14070
+ scope: domainId === null ? "catch-all" : "domain"
14071
+ });
14072
+ }
14073
+ return matches;
14074
+ }
14075
+ function formatFunctionEndpointNoiseWarning(params) {
14076
+ if (params.endpoints.filter((endpoint) => !endpoint.is_current_function).length === 0) return null;
14077
+ const lines = [`Warning: ${params.endpoints.length} function endpoints may receive mail for ${params.toAddress}:`];
14078
+ for (const endpoint of params.endpoints) {
14079
+ const scope = endpoint.scope === "catch-all" ? "catch-all" : `scoped to ${params.inboundDomain}`;
14080
+ const current = endpoint.is_current_function ? " (this function)" : "";
14081
+ const target = endpoint.function_id ? ` -> function ${endpoint.function_id}` : "";
14082
+ lines.push(`- endpoint ${endpoint.id}${target}, ${scope}${current}`);
14083
+ }
14084
+ return lines.join("\n");
14085
+ }
14086
+ async function maybeWriteEndpointNoiseWarning(params) {
14087
+ try {
14088
+ const [domainsResult, endpointsResult] = await Promise.all([listDomains({
14089
+ client: params.apiClient.client,
14090
+ responseStyle: "fields"
14091
+ }), listEndpoints({
14092
+ client: params.apiClient.client,
14093
+ responseStyle: "fields"
14094
+ })]);
14095
+ if (endpointsResult.error) return;
14096
+ if (domainsResult.error) return;
14097
+ const inboundDomainId = domainsResult.data?.data?.find((domain) => domain.domain?.toLowerCase() === params.invocation.inbound_domain.toLowerCase())?.id ?? null;
14098
+ const endpointsEnvelope = endpointsResult.data;
14099
+ const warning = formatFunctionEndpointNoiseWarning({
14100
+ endpoints: findMatchingFunctionEndpoints({
14101
+ currentFunctionId: params.currentFunctionId,
14102
+ endpoints: endpointsEnvelope?.data ?? [],
14103
+ inboundDomainId
14104
+ }),
14105
+ inboundDomain: params.invocation.inbound_domain,
14106
+ toAddress: params.invocation.to
14107
+ });
14108
+ if (warning) params.writeStderr(`${warning}\n`);
14109
+ } catch {}
14110
+ }
12344
14111
  var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Command {
12345
14112
  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
14113
  static summary = "Trigger a test invocation; with --wait, watch it land";
@@ -12424,11 +14191,19 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
12424
14191
  this.log(JSON.stringify(invocation, null, 2));
12425
14192
  return;
12426
14193
  }
14194
+ await maybeWriteEndpointNoiseWarning({
14195
+ apiClient,
14196
+ currentFunctionId: flags.id,
14197
+ invocation,
14198
+ writeStderr: (chunk) => {
14199
+ process.stderr.write(chunk);
14200
+ }
14201
+ });
12427
14202
  const startedAt = Date.now();
12428
14203
  const timeoutMs = flags.timeout * 1e3;
12429
14204
  const pollIntervalMs = flags["poll-interval"] * 1e3;
12430
14205
  const isExpired = () => flags.timeout > 0 && Date.now() - startedAt > timeoutMs;
12431
- this.log(`Waiting for test inbound to arrive at ${invocation.to}...`);
14206
+ writeFunctionTestProgress(`Waiting for test inbound to arrive at ${invocation.to}...`);
12432
14207
  let inboundId;
12433
14208
  while (!isExpired()) {
12434
14209
  const page = await fetchEmailSearchPage({
@@ -12457,7 +14232,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
12457
14232
  await sleep$1(pollIntervalMs);
12458
14233
  }
12459
14234
  if (!inboundId) this.error(`Timed out after ${flags.timeout}s waiting for test inbound ${invocation.to} to land. Browse ${invocation.watch_url} for the live view.`, { exit: 2 });
12460
- this.log(`Inbound landed (${inboundId}). Waiting for function to run...`);
14235
+ writeFunctionTestProgress(`Inbound landed (${inboundId}). Waiting for function to run...`);
12461
14236
  let detail;
12462
14237
  while (!isExpired()) {
12463
14238
  const result = await getEmail({
@@ -12486,17 +14261,14 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
12486
14261
  }
12487
14262
  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
14263
  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;
14264
+ const outcome = buildFunctionTestOutcome({
14265
+ detail,
14266
+ elapsedSeconds,
14267
+ functionId: flags.id,
14268
+ inboundId,
14269
+ invocation,
14270
+ showSends: shouldShowSends
14271
+ });
12500
14272
  this.log(JSON.stringify(outcome, null, 2));
12501
14273
  if (detail.webhook_status === "exhausted") process.exitCode = 1;
12502
14274
  });
@@ -12763,6 +14535,106 @@ var LogoutCommand = class LogoutCommand extends Command {
12763
14535
  }
12764
14536
  };
12765
14537
  //#endregion
14538
+ //#region src/oclif/message-body-sources.ts
14539
+ function defaultReadFile(path) {
14540
+ return readFileSync(path, "utf8");
14541
+ }
14542
+ function defaultReadStdin() {
14543
+ if (process.stdin.isTTY) throw new Error("stdin is a TTY; pipe a value into this command or pass a file/string source instead.");
14544
+ return readFileSync(0, "utf8");
14545
+ }
14546
+ function selectedSources(sources) {
14547
+ return sources.filter(([, selected]) => selected).map(([label]) => label);
14548
+ }
14549
+ function readTextFile(path, label, readFile) {
14550
+ try {
14551
+ return {
14552
+ content: readFile(path),
14553
+ kind: "ok"
14554
+ };
14555
+ } catch (error) {
14556
+ return {
14557
+ kind: "error",
14558
+ message: `Could not read ${label} ${path}: ${error instanceof Error ? error.message : String(error)}`
14559
+ };
14560
+ }
14561
+ }
14562
+ function readTextStdin(label, readStdin) {
14563
+ try {
14564
+ return {
14565
+ content: readStdin(),
14566
+ kind: "ok"
14567
+ };
14568
+ } catch (error) {
14569
+ return {
14570
+ kind: "error",
14571
+ message: `Could not read ${label}: ${error instanceof Error ? error.message : String(error)}`
14572
+ };
14573
+ }
14574
+ }
14575
+ function resolveMessageBodies(input) {
14576
+ const bodySources = selectedSources([
14577
+ ["--body", input.body !== void 0],
14578
+ ["--body-file", input.bodyFile !== void 0],
14579
+ ["--body-stdin", input.bodyStdin === true]
14580
+ ]);
14581
+ if (bodySources.length > 1) return {
14582
+ kind: "error",
14583
+ message: `Pass only one plain-text body source (got ${bodySources.join(", ")}).`
14584
+ };
14585
+ const htmlSources = selectedSources([
14586
+ ["--html", input.html !== void 0],
14587
+ ["--html-file", input.htmlFile !== void 0],
14588
+ ["--html-stdin", input.htmlStdin === true]
14589
+ ]);
14590
+ if (htmlSources.length > 1) return {
14591
+ kind: "error",
14592
+ message: `Pass only one HTML body source (got ${htmlSources.join(", ")}).`
14593
+ };
14594
+ const stdinSources = selectedSources([["--body-stdin", input.bodyStdin === true], ["--html-stdin", input.htmlStdin === true]]);
14595
+ if (stdinSources.length > 1) return {
14596
+ kind: "error",
14597
+ message: `Stdin can only be consumed once (got ${stdinSources.join(", ")}).`
14598
+ };
14599
+ if (bodySources.length === 0 && htmlSources.length === 0) return {
14600
+ kind: "error",
14601
+ message: "Either a plain-text body source or an HTML body source is required."
14602
+ };
14603
+ const readFile = input.readFile ?? defaultReadFile;
14604
+ const readStdin = input.readStdin ?? defaultReadStdin;
14605
+ let body = input.body;
14606
+ let html = input.html;
14607
+ if (input.bodyFile !== void 0) {
14608
+ const result = readTextFile(input.bodyFile, "--body-file", readFile);
14609
+ if (result.kind === "error") return result;
14610
+ body = result.content;
14611
+ }
14612
+ if (input.bodyStdin === true) {
14613
+ const result = readTextStdin("--body-stdin", readStdin);
14614
+ if (result.kind === "error") return result;
14615
+ body = result.content;
14616
+ }
14617
+ if (input.htmlFile !== void 0) {
14618
+ const result = readTextFile(input.htmlFile, "--html-file", readFile);
14619
+ if (result.kind === "error") return result;
14620
+ html = result.content;
14621
+ }
14622
+ if (input.htmlStdin === true) {
14623
+ const result = readTextStdin("--html-stdin", readStdin);
14624
+ if (result.kind === "error") return result;
14625
+ html = result.content;
14626
+ }
14627
+ if (!body && !html) return {
14628
+ kind: "error",
14629
+ message: "Either a non-empty plain-text body or a non-empty HTML body is required."
14630
+ };
14631
+ return {
14632
+ ...body !== void 0 ? { body } : {},
14633
+ ...html !== void 0 ? { html } : {},
14634
+ kind: "ok"
14635
+ };
14636
+ }
14637
+ //#endregion
12766
14638
  //#region src/oclif/commands/reply.ts
12767
14639
  var ReplyCommand = class ReplyCommand extends Command {
12768
14640
  static description = `Reply to an inbound email.
@@ -12771,6 +14643,7 @@ var ReplyCommand = class ReplyCommand extends Command {
12771
14643
  static summary = "Reply to an inbound email";
12772
14644
  static examples = [
12773
14645
  "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
14646
+ "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
12774
14647
  "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
12775
14648
  "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
12776
14649
  ];
@@ -12794,7 +14667,11 @@ var ReplyCommand = class ReplyCommand extends Command {
12794
14667
  required: true
12795
14668
  }),
12796
14669
  body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
14670
+ "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
14671
+ "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
12797
14672
  html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
14673
+ "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
14674
+ "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
12798
14675
  from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
12799
14676
  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." }),
12800
14677
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
@@ -12802,7 +14679,15 @@ var ReplyCommand = class ReplyCommand extends Command {
12802
14679
  };
12803
14680
  async run() {
12804
14681
  const { flags } = await this.parse(ReplyCommand);
12805
- if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
14682
+ const bodies = resolveMessageBodies({
14683
+ body: flags.body,
14684
+ bodyFile: flags["body-file"],
14685
+ bodyStdin: flags["body-stdin"],
14686
+ html: flags.html,
14687
+ htmlFile: flags["html-file"],
14688
+ htmlStdin: flags["html-stdin"]
14689
+ });
14690
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
12806
14691
  await runWithTiming(flags.time, async () => {
12807
14692
  const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12808
14693
  const auth = resolveCliAuth({
@@ -12818,8 +14703,8 @@ var ReplyCommand = class ReplyCommand extends Command {
12818
14703
  });
12819
14704
  const result = await replyToEmail({
12820
14705
  body: {
12821
- ...flags.body !== void 0 ? { body_text: flags.body } : {},
12822
- ...flags.html !== void 0 ? { body_html: flags.html } : {},
14706
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
14707
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
12823
14708
  ...flags.from !== void 0 ? { from: flags.from } : {},
12824
14709
  ...flags.wait !== void 0 ? { wait: flags.wait } : {},
12825
14710
  ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
@@ -12894,6 +14779,7 @@ var SendCommand = class SendCommand extends Command {
12894
14779
  static summary = "Send an email (simplified, agent-friendly)";
12895
14780
  static examples = [
12896
14781
  "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
14782
+ "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
12897
14783
  "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
12898
14784
  "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
12899
14785
  "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
@@ -12921,7 +14807,11 @@ var SendCommand = class SendCommand extends Command {
12921
14807
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
12922
14808
  subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
12923
14809
  body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
14810
+ "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
14811
+ "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
12924
14812
  html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
14813
+ "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
14814
+ "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
12925
14815
  "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>`." }),
12926
14816
  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." }),
12927
14817
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
@@ -12929,7 +14819,15 @@ var SendCommand = class SendCommand extends Command {
12929
14819
  };
12930
14820
  async run() {
12931
14821
  const { flags } = await this.parse(SendCommand);
12932
- if (!flags.body && !flags.html) throw new Errors.CLIError("Either --body or --html (or both) is required.");
14822
+ const bodies = resolveMessageBodies({
14823
+ body: flags.body,
14824
+ bodyFile: flags["body-file"],
14825
+ bodyStdin: flags["body-stdin"],
14826
+ html: flags.html,
14827
+ htmlFile: flags["html-file"],
14828
+ htmlStdin: flags["html-stdin"]
14829
+ });
14830
+ if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
12933
14831
  await runWithTiming(flags.time, async () => {
12934
14832
  const baseUrlOverridden = flags["api-base-url-1"] !== void 0 || flags["api-base-url-2"] !== void 0;
12935
14833
  const auth = resolveCliAuth({
@@ -12949,14 +14847,14 @@ var SendCommand = class SendCommand extends Command {
12949
14847
  configDir: this.config.configDir
12950
14848
  };
12951
14849
  const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
12952
- const subject = flags.subject ?? (flags.body ? deriveSubject(flags.body) : "Message");
14850
+ const subject = flags.subject ?? (bodies.body ? deriveSubject(bodies.body) : "Message");
12953
14851
  const result = await sendEmail({
12954
14852
  body: {
12955
14853
  from,
12956
14854
  to: flags.to,
12957
14855
  subject,
12958
- ...flags.body !== void 0 ? { body_text: flags.body } : {},
12959
- ...flags.html !== void 0 ? { body_html: flags.html } : {},
14856
+ ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
14857
+ ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
12960
14858
  ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
12961
14859
  ...flags.wait !== void 0 ? { wait: flags.wait } : {},
12962
14860
  ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
@@ -13496,6 +15394,7 @@ function renderFishCompletion(binName) {
13496
15394
  lines.push(`complete -c ${binName} -f -n '__fish_${binName}_topic_needs_subcommand ${fishEscape(topic)}' -a '${fishEscape(operation.command)}' -d '${fishEscape(operation.summary ?? `${operation.method} ${operation.path}`)}'`);
13497
15395
  for (const parameter of [...operation.pathParams, ...operation.queryParams]) lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l '${fishEscape(parameter.name.replace(/_/g, "-"))}' -r -d '${fishEscape(parameter.description ?? parameter.name)}'`);
13498
15396
  lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'api-key' -r -d 'Primitive API key (defaults to PRIMITIVE_API_KEY or saved primitive login credentials)'`);
15397
+ if (!operation.binaryResponse) lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'envelope' -d 'Print the full response envelope, including pagination metadata'`);
13499
15398
  if (operation.hasJsonBody) lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body' -r -d 'JSON request body'`, `complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'body-file' -r -d 'Path to a JSON file used as the request body'`);
13500
15399
  if (operation.binaryResponse) lines.push(`complete -c ${binName} -n '${operationCondition(operation).replace(BIN_PLACEHOLDER, binName)}' -l 'output' -r -d 'Write binary response bytes to a file'`);
13501
15400
  }
@@ -13613,7 +15512,6 @@ const CANONICAL_OPERATION_ALIASES = {
13613
15512
  "functions:get": "functions:get-function",
13614
15513
  "functions:list": "functions:list-functions",
13615
15514
  "functions:list-secrets": "functions:list-function-secrets",
13616
- "functions:logs": "functions:list-function-logs",
13617
15515
  "sending:get": "sending:get-sent-email",
13618
15516
  "sending:list": "sending:list-sent-emails",
13619
15517
  "sending:permissions": "sending:get-send-permissions",
@@ -13626,6 +15524,7 @@ const CANONICAL_OPERATION_ALIASES = {
13626
15524
  };
13627
15525
  const DESCRIBE_OPERATION_ALIASES = {
13628
15526
  ...CANONICAL_OPERATION_ALIASES,
15527
+ "functions:logs": "functions:list-function-logs",
13629
15528
  reply: "sending:reply-to-email"
13630
15529
  };
13631
15530
  function resolveOperationAlias(id) {
@@ -13648,6 +15547,7 @@ const COMMANDS = {
13648
15547
  "emails:watch": EmailsWatchCommand,
13649
15548
  "emails:wait": EmailsWaitCommand,
13650
15549
  "functions:init": FunctionsInitCommand,
15550
+ "functions:templates": FunctionsTemplatesCommand,
13651
15551
  "functions:deploy": FunctionsDeployCommand,
13652
15552
  "functions:redeploy": FunctionsRedeployCommand,
13653
15553
  "functions:set-secret": FunctionsSetSecretCommand,
@@ -13658,7 +15558,8 @@ const COMMANDS = {
13658
15558
  if (!command) throw new Error(`Missing generated command target for alias ${alias}`);
13659
15559
  return [alias, command];
13660
15560
  })),
13661
- ...generatedCommands
15561
+ ...generatedCommands,
15562
+ "functions:logs": FunctionsLogsCommand
13662
15563
  };
13663
15564
  //#endregion
13664
15565
  export { CANONICAL_OPERATION_ALIASES, COMMANDS, lookupOperation };