@primitivedotdev/sdk 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # `@primitivedotdev/sdk`
2
2
 
3
- The official Node.js SDK and command-line tool for [Primitive](https://primitive.dev), an email API for sending and receiving programmatic mail.
3
+ The official Node.js library for [Primitive](https://primitive.dev), an email API for sending and receiving programmatic mail. Typed client for receiving and verifying inbound webhooks, sending mail, parsing raw MIME, and calling the full HTTP API.
4
4
 
5
- The package ships two things in one install:
5
+ ## Looking for the CLI?
6
6
 
7
- - A **`primitive` CLI** for interactive use, scripts, and agent workflows. Sends mail, reads inbound, inspects send history, manages domains and webhook endpoints, all in one binary.
8
- - A **typed Node library** for programmatic integration in app code. Receives and verifies inbound webhooks, sends mail, parses raw MIME, and exposes the full HTTP API as generated functions.
7
+ The `primitive` CLI now ships as a separate package, [`@primitivedotdev/cli`](https://www.npmjs.com/package/@primitivedotdev/cli). Install it with:
9
8
 
10
- Pick whichever fits the call site. The two share the same auth (`PRIMITIVE_API_KEY`), the same data shapes, and the same OpenAPI spec.
9
+ ```bash
10
+ npm install -g @primitivedotdev/cli
11
+ # or, no-install:
12
+ npx @primitivedotdev/cli@latest <command>
13
+ ```
14
+
15
+ This package retains the CLI for a few minor releases for backward compatibility (`npx @primitivedotdev/sdk@latest <command>` still works and prints a one-line migration banner). The retained CLI snapshot will be removed in a future minor release.
11
16
 
12
17
  ## Install
13
18
 
@@ -15,56 +20,16 @@ Pick whichever fits the call site. The two share the same auth (`PRIMITIVE_API_K
15
20
  npm install @primitivedotdev/sdk
16
21
  ```
17
22
 
18
- For one-off CLI use, `npx @primitivedotdev/sdk@latest <command>` works without installing.
19
-
20
23
  Requires Node.js 22 or newer.
21
24
 
22
25
  ## Set your API key
23
26
 
24
- Get a key from your [dashboard](https://primitive.dev) and export it. Both the CLI and the library default to reading `PRIMITIVE_API_KEY` from the environment.
27
+ Get a key from your [dashboard](https://primitive.dev) and export it. The library defaults to reading `PRIMITIVE_API_KEY` from the environment.
25
28
 
26
29
  ```bash
27
30
  export PRIMITIVE_API_KEY=prim_...
28
31
  ```
29
32
 
30
- ## Command line
31
-
32
- Everything below assumes `PRIMITIVE_API_KEY` is set. Each command also accepts `--api-key <value>` if you want to pass it explicitly.
33
-
34
- ```bash
35
- # Confirm the key is live and see which account it authenticates.
36
- primitive whoami
37
-
38
- # Send an email. --wait blocks until the receiving MTA returns a delivery
39
- # outcome (a synchronous SMTP 250 from Gmail, etc.); without it, the call
40
- # returns once Primitive has accepted the message for delivery.
41
- primitive send --to alice@example.com --body "Hi Alice!" --wait
42
-
43
- # See the most recent inbound emails as a compact text table.
44
- # IDs are full UUIDs when piped, truncated for interactive terminals.
45
- primitive emails:latest --limit 5
46
-
47
- # Read one inbound's full record (body, headers, threading metadata).
48
- primitive emails:get-email --id <uuid>
49
-
50
- # Reply to an inbound. Threading and the "Re:" subject are derived
51
- # server-side from the parent message; you supply only the body.
52
- primitive sending:reply-to-email --id <inbound-id> --body-text "..."
53
-
54
- # See where you are allowed to send. Returns a typed list of
55
- # permission rules (managed-zone wildcards, your own verified domains,
56
- # specific addresses with grants). The send-mail call enforces these
57
- # at request time.
58
- primitive sending:get-send-permissions
59
-
60
- # Look up an operation's request/response schema, including per-field
61
- # descriptions sourced from the OpenAPI spec.
62
- primitive describe emails:get-email
63
- primitive describe sending:send-email
64
- ```
65
-
66
- Run `primitive --help` for the full topic list and `primitive <topic> --help` for the commands within each. Every command accepts `--help`, and the descriptions are detailed enough that the CLI is self-documenting for most workflows.
67
-
68
33
  ## Library
69
34
 
70
35
  The default root import is intentionally small and centered on the two most common app-code use cases: receiving inbound webhook deliveries and sending mail.
package/bin/run.js CHANGED
@@ -1,5 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // The CLI moved to @primitivedotdev/cli. This bin is retained on
4
+ // @primitivedotdev/sdk for a few minor versions so existing scripts
5
+ // and agent prompts that invoke `npx @primitivedotdev/sdk@latest
6
+ // <command>` keep working. Every invocation prints a one-line stderr
7
+ // banner with the migration command; the actual oclif runtime runs
8
+ // unchanged after that. The CLI surface will be removed from
9
+ // @primitivedotdev/sdk in a future minor release; until then this
10
+ // shipped snapshot is frozen against the 0.23.0 command set.
3
11
  import { execute } from "@oclif/core";
4
12
 
13
+ process.stderr.write(
14
+ "[@primitivedotdev/sdk] Heads up: the CLI moved to @primitivedotdev/cli. " +
15
+ "Switch to `npx @primitivedotdev/cli@latest <command>` " +
16
+ "(or `npm install -g @primitivedotdev/cli`). " +
17
+ "The CLI surface will be removed from @primitivedotdev/sdk in a future minor release.\n",
18
+ );
19
+
5
20
  await execute({ dir: import.meta.url });
@@ -3,6 +3,7 @@ import { createFunction } from "../../api/generated/sdk.gen.js";
3
3
  import { PrimitiveApiClient } from "../../api/index.js";
4
4
  import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
5
  import { resolveCliAuth } from "../auth.js";
6
+ import { emitRawSendMailFetchWarning } from "../lint/raw-send-mail-fetch.js";
6
7
  // `primitive functions:deploy` is the agent-grade shortcut for
7
8
  // `functions:create-function`. The underlying operation takes `code`
8
9
  // as a string in the JSON body, which is awkward at the CLI for
@@ -72,6 +73,12 @@ class FunctionsDeployCommand extends Command {
72
73
  const sourceMap = flags["source-map-file"]
73
74
  ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
74
75
  : undefined;
76
+ // Non-blocking deploy-time lint: if the bundle has a raw
77
+ // fetch(...) call against /send-mail, nudge the author toward
78
+ // `createPrimitiveClient` from `@primitivedotdev/sdk/api`.
79
+ // The warning lands on stderr so it never contaminates the
80
+ // JSON stdout the caller may pipe into jq.
81
+ emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
75
82
  const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
76
83
  flags["api-base-url-2"] !== undefined;
77
84
  const auth = resolveCliAuth({
@@ -3,6 +3,7 @@ import { updateFunction } from "../../api/generated/sdk.gen.js";
3
3
  import { PrimitiveApiClient } from "../../api/index.js";
4
4
  import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
5
  import { resolveCliAuth } from "../auth.js";
6
+ import { emitRawSendMailFetchWarning } from "../lint/raw-send-mail-fetch.js";
6
7
  // `primitive functions:redeploy` is the agent-grade shortcut for
7
8
  // `functions:update-function`. Same file-reading ergonomic as
8
9
  // functions:deploy but for an existing function. Use this to push a
@@ -61,6 +62,12 @@ class FunctionsRedeployCommand extends Command {
61
62
  const sourceMap = flags["source-map-file"]
62
63
  ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
63
64
  : undefined;
65
+ // Non-blocking deploy-time lint: if the bundle has a raw
66
+ // fetch(...) call against /send-mail, nudge the author toward
67
+ // `createPrimitiveClient` from `@primitivedotdev/sdk/api`.
68
+ // Same check as functions:deploy; warning goes to stderr and
69
+ // the deploy continues regardless.
70
+ emitRawSendMailFetchWarning(code, (chunk) => process.stderr.write(chunk));
64
71
  const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
65
72
  flags["api-base-url-2"] !== undefined;
66
73
  const auth = resolveCliAuth({
@@ -0,0 +1,98 @@
1
+ // Deploy-time lint for `functions:deploy --file <bundle>` and
2
+ // `functions:redeploy --file <bundle>`. Looks for a raw
3
+ // `fetch("...primitive.dev/.../send-mail", ...)` call in the bundle
4
+ // text and, on a match, emits a stderr warning telling the author
5
+ // to prefer `createPrimitiveClient` from `@primitivedotdev/sdk/api`.
6
+ //
7
+ // Why: a recurring pattern in agent-assisted Function deploys is to
8
+ // copy the REST snippet from the docs and call `fetch` directly
9
+ // against the Primitive send-mail endpoint, even after the docs
10
+ // switched to leading with the SDK and `functions:init` ships a
11
+ // scaffold that uses `createPrimitiveClient`. The SDK already
12
+ // handles dual-host routing, error envelopes, and send-permission
13
+ // gate denials; raw `fetch` re-discovers each of those by hand. The
14
+ // warning is the catch-net at deploy time: the deploy still
15
+ // proceeds.
16
+ //
17
+ // Scope by design:
18
+ // - We only flag the empirically observed footgun: `/send-mail`.
19
+ // Not arbitrary calls to api.primitive.dev. Other endpoints have
20
+ // not surfaced the same pattern.
21
+ // - We require the URL string literal to immediately follow the
22
+ // `fetch(` token (allowing whitespace) so we don't trip on a
23
+ // comment that merely mentions the URL. esbuild strips most
24
+ // comments anyway, but anchoring on `fetch(` keeps the rule
25
+ // honest without trying to parse JS.
26
+ // - We only look at the bundle text passed in. Source maps and
27
+ // sibling files are not scanned.
28
+ // - Variable-URL cases (`fetch(url, ...)` where `url` was assembled
29
+ // elsewhere) are accepted false negatives. The value here is
30
+ // catching the obvious inline-literal case, not full taint
31
+ // analysis.
32
+ // Match `fetch(` then a string literal (single, double, or backtick)
33
+ // whose contents include `primitive.dev` and end with `/send-mail`
34
+ // (optionally with a query string or trailing path boundary). Examples
35
+ // matched:
36
+ // fetch("https://api.primitive.dev/v1/send-mail", {...})
37
+ // fetch(`https://www.primitive.dev/api/v1/send-mail`, {...})
38
+ // fetch('https://primitive.dev/api/v1/send-mail?wait=1')
39
+ //
40
+ // The `[^`'"]*` inside the literal forbids the closing quote
41
+ // character itself so we can't accidentally span across two adjacent
42
+ // string literals. The trailing `(?![A-Za-z0-9_-])` forbids any
43
+ // letter, digit, underscore, or hyphen immediately after `send-mail`
44
+ // so `/send-mail-template-preview` does not trip the rule. (Plain
45
+ // `\b` does not help here because `-` is itself a non-word character,
46
+ // so `mail\b` still matches `mail-...`.)
47
+ const RAW_SEND_MAIL_FETCH_REGEX = /fetch\s*\(\s*[`'"][^`'"]*primitive\.dev[^`'"]*\/send-mail(?![A-Za-z0-9_-])/g;
48
+ // How much surrounding text to include on either side of the match
49
+ // when building the sample snippet. Kept short on purpose: bundles
50
+ // are minified and a 120-char window is enough to spot the call
51
+ // without flooding stderr.
52
+ const SNIPPET_PADDING = 60;
53
+ export function detectRawSendMailFetch(bundleText) {
54
+ // Reset lastIndex defensively: this regex is module-scoped with
55
+ // the /g flag, so a prior call's state would skip the next match.
56
+ RAW_SEND_MAIL_FETCH_REGEX.lastIndex = 0;
57
+ const match = RAW_SEND_MAIL_FETCH_REGEX.exec(bundleText);
58
+ if (!match) {
59
+ return { found: false, sampleSnippet: null };
60
+ }
61
+ const start = Math.max(0, match.index - SNIPPET_PADDING);
62
+ const end = Math.min(bundleText.length, match.index + match[0].length + SNIPPET_PADDING);
63
+ const raw = bundleText.slice(start, end);
64
+ // Collapse newlines and runs of whitespace so the snippet is one
65
+ // readable line on stderr regardless of how the bundle was
66
+ // formatted.
67
+ const sampleSnippet = raw.replace(/\s+/g, " ").trim();
68
+ return { found: true, sampleSnippet };
69
+ }
70
+ // The stderr warning copy. Three beats: name the issue, name the
71
+ // SDK alternative, link the docs. Plus a one-line "deploy proceeds"
72
+ // reassurance. Kept punctuation simple (commas, periods, line
73
+ // breaks) so it doesn't trip the no-em-dashes hook and so the lines
74
+ // wrap predictably in a terminal.
75
+ export const RAW_SEND_MAIL_FETCH_WARNING_LINES = [
76
+ "warning: this bundle calls fetch(...) against /send-mail directly.",
77
+ "The Primitive SDK exposes createPrimitiveClient from",
78
+ "@primitivedotdev/sdk/api which handles host routing, error envelopes,",
79
+ "and gate denials for you. See https://www.primitive.dev/docs/functions",
80
+ "for the recommended in-handler pattern. Continuing with deploy.",
81
+ ];
82
+ export function formatRawSendMailFetchWarning(finding) {
83
+ const lines = [...RAW_SEND_MAIL_FETCH_WARNING_LINES];
84
+ if (finding.sampleSnippet) {
85
+ lines.push(` found: ${finding.sampleSnippet}`);
86
+ }
87
+ return `${lines.join("\n")}\n`;
88
+ }
89
+ // Convenience: run the detector and, on a match, write the warning
90
+ // to a stderr-shaped writer. Pulled out so both deploy and redeploy
91
+ // share one code path and so the unit tests can pass a fake writer.
92
+ export function emitRawSendMailFetchWarning(bundleText, write) {
93
+ const finding = detectRawSendMailFetch(bundleText);
94
+ if (finding.found) {
95
+ write(formatRawSendMailFetchWarning(finding));
96
+ }
97
+ return finding;
98
+ }
@@ -4283,5 +4283,5 @@
4283
4283
  "enableJsonFlag": false
4284
4284
  }
4285
4285
  },
4286
- "version": "0.23.0"
4286
+ "version": "0.24.0"
4287
4287
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "0.23.0",
4
- "description": "Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser modules",
3
+ "version": "0.24.0",
4
+ "description": "Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules. The CLI moved to @primitivedotdev/cli; this package retains the CLI as a deprecated alias for a few minor releases.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",