@primitivedotdev/sdk 0.22.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 +11 -46
- package/bin/run.js +15 -0
- package/dist/oclif/commands/functions-deploy.js +7 -0
- package/dist/oclif/commands/functions-init.js +256 -0
- package/dist/oclif/commands/functions-redeploy.js +7 -0
- package/dist/oclif/index.js +8 -0
- package/dist/oclif/lint/raw-send-mail-fetch.js +98 -0
- package/oclif.manifest.json +34 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# `@primitivedotdev/sdk`
|
|
2
2
|
|
|
3
|
-
The official Node.js
|
|
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
|
-
|
|
5
|
+
## Looking for the CLI?
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
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({
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { Args, Command, Errors, Flags } from "@oclif/core";
|
|
4
|
+
// `primitive functions:init <name>` stamps a deployable Function project
|
|
5
|
+
// into ./<name>/ so a new author can go from zero to a deployed handler
|
|
6
|
+
// in two commands: `npm install && npm run build` then
|
|
7
|
+
// `primitive functions:deploy --name <name> --file ./dist/handler.js`.
|
|
8
|
+
//
|
|
9
|
+
// The scaffolded handler imports `createPrimitiveClient` from
|
|
10
|
+
// `@primitivedotdev/sdk/api`, NOT from the package root. The root export
|
|
11
|
+
// pulls in webhook helpers that depend on `node:crypto`, which breaks
|
|
12
|
+
// Workers-style bundles. The `/api` subpath is the runtime-client
|
|
13
|
+
// surface and is the documented import for in-handler use.
|
|
14
|
+
// The SDK version range that ships in the scaffolded package.json's
|
|
15
|
+
// dependencies. Pinned to the current shipped minor with a caret so
|
|
16
|
+
// patch releases of the SDK pick up automatically. Update alongside
|
|
17
|
+
// any major version bump of the SDK.
|
|
18
|
+
const SDK_VERSION_RANGE = "^0.22.0";
|
|
19
|
+
// esbuild version range. Pinned to the latest stable major used
|
|
20
|
+
// elsewhere in the Primitive codebase for bundling Workers-style
|
|
21
|
+
// handlers. Caret range so patch fixes flow in automatically.
|
|
22
|
+
const ESBUILD_VERSION_RANGE = "^0.27.0";
|
|
23
|
+
// Validate a directory name passed as the positional argument.
|
|
24
|
+
// Matches a conservative slug shape: lowercase letters, digits,
|
|
25
|
+
// hyphens, underscores. Rejecting weirder names up front prevents
|
|
26
|
+
// surprises when the same string lands in package.json's `name`
|
|
27
|
+
// field (which has its own validation rules) or in shell scripts.
|
|
28
|
+
const VALID_NAME = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
29
|
+
export function isValidFunctionName(name) {
|
|
30
|
+
return VALID_NAME.test(name);
|
|
31
|
+
}
|
|
32
|
+
// File contents for the scaffolded project. Each renderer takes the
|
|
33
|
+
// function name and returns the raw file body. Kept as named exports
|
|
34
|
+
// so the unit test can assert content without having to spin up the
|
|
35
|
+
// oclif command lifecycle.
|
|
36
|
+
export function renderHandler() {
|
|
37
|
+
return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
|
|
38
|
+
import { createPrimitiveClient } from "@primitivedotdev/sdk/api";
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
async fetch(
|
|
42
|
+
req: Request,
|
|
43
|
+
env: { PRIMITIVE_API_KEY: string },
|
|
44
|
+
): Promise<Response> {
|
|
45
|
+
const event = (await req.json()) as {
|
|
46
|
+
email: { headers: { from?: string; subject?: string } };
|
|
47
|
+
};
|
|
48
|
+
const client = createPrimitiveClient({ apiKey: env.PRIMITIVE_API_KEY });
|
|
49
|
+
|
|
50
|
+
const reply = await client.send({
|
|
51
|
+
from: "you@your-domain.primitive.email",
|
|
52
|
+
to: event.email.headers.from ?? "you@your-domain.primitive.email",
|
|
53
|
+
subject: \`Re: \${event.email.headers.subject ?? ""}\`,
|
|
54
|
+
bodyText: "Got your message.",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return Response.json({ ok: true, reply });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
export function renderPackageJson(name) {
|
|
63
|
+
const pkg = {
|
|
64
|
+
name,
|
|
65
|
+
version: "0.1.0",
|
|
66
|
+
private: true,
|
|
67
|
+
type: "module",
|
|
68
|
+
scripts: {
|
|
69
|
+
build: "node build.mjs",
|
|
70
|
+
deploy: `npm run build && primitive functions:deploy --name ${name} --file ./dist/handler.js`,
|
|
71
|
+
redeploy: "npm run build && primitive functions:redeploy --id $PRIMITIVE_FUNCTION_ID --file ./dist/handler.js",
|
|
72
|
+
},
|
|
73
|
+
dependencies: {
|
|
74
|
+
"@primitivedotdev/sdk": SDK_VERSION_RANGE,
|
|
75
|
+
},
|
|
76
|
+
devDependencies: {
|
|
77
|
+
esbuild: ESBUILD_VERSION_RANGE,
|
|
78
|
+
typescript: "^5.7.2",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
82
|
+
}
|
|
83
|
+
export function renderBuildMjs() {
|
|
84
|
+
return `import { build } from "esbuild";
|
|
85
|
+
|
|
86
|
+
// Bundle handler.ts into a single ESM file suitable for the Primitive
|
|
87
|
+
// Functions runtime. The runtime is a Workers-style environment, so
|
|
88
|
+
// we pick the "worker" / "browser" export conditions on @primitivedotdev/sdk
|
|
89
|
+
// (which routes us to the /api subpath safely without dragging in
|
|
90
|
+
// node:crypto-dependent webhook helpers).
|
|
91
|
+
|
|
92
|
+
await build({
|
|
93
|
+
entryPoints: ["handler.ts"],
|
|
94
|
+
bundle: true,
|
|
95
|
+
format: "esm",
|
|
96
|
+
platform: "browser",
|
|
97
|
+
target: "es2022",
|
|
98
|
+
conditions: ["worker", "browser"],
|
|
99
|
+
outfile: "dist/handler.js",
|
|
100
|
+
});
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
export function renderTsconfig() {
|
|
104
|
+
const tsconfig = {
|
|
105
|
+
compilerOptions: {
|
|
106
|
+
target: "ES2022",
|
|
107
|
+
module: "ESNext",
|
|
108
|
+
moduleResolution: "Bundler",
|
|
109
|
+
strict: true,
|
|
110
|
+
lib: ["ES2022", "WebWorker"],
|
|
111
|
+
types: [],
|
|
112
|
+
esModuleInterop: true,
|
|
113
|
+
skipLibCheck: true,
|
|
114
|
+
},
|
|
115
|
+
include: ["handler.ts"],
|
|
116
|
+
};
|
|
117
|
+
return `${JSON.stringify(tsconfig, null, 2)}\n`;
|
|
118
|
+
}
|
|
119
|
+
export function renderGitignore() {
|
|
120
|
+
return "node_modules\ndist\n";
|
|
121
|
+
}
|
|
122
|
+
export function renderReadme(name) {
|
|
123
|
+
return `# ${name}
|
|
124
|
+
|
|
125
|
+
## What this is
|
|
126
|
+
|
|
127
|
+
A Primitive Function: a JavaScript handler that runs on inbound mail.
|
|
128
|
+
It receives the \`email.received\` event, demonstrates a basic reply
|
|
129
|
+
via the Primitive SDK, and returns a JSON envelope.
|
|
130
|
+
|
|
131
|
+
## Develop
|
|
132
|
+
|
|
133
|
+
\`\`\`
|
|
134
|
+
npm install
|
|
135
|
+
npm run build
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
## Deploy
|
|
139
|
+
|
|
140
|
+
\`\`\`
|
|
141
|
+
npm run deploy
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
The deploy step calls \`primitive functions:deploy\` and requires
|
|
145
|
+
\`PRIMITIVE_API_KEY\` to be set in your shell (or pass \`--api-key\`).
|
|
146
|
+
Run \`primitive login\` once to save a key in your CLI config if you
|
|
147
|
+
prefer that to an env var.
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
// Files written by the scaffolder, in the order they're created.
|
|
151
|
+
// Exported as a pure function so the unit test can verify the
|
|
152
|
+
// exact content of every file without invoking the command and
|
|
153
|
+
// touching disk.
|
|
154
|
+
export function scaffoldFiles(name) {
|
|
155
|
+
return [
|
|
156
|
+
{ contents: renderHandler(), relativePath: "handler.ts" },
|
|
157
|
+
{ contents: renderPackageJson(name), relativePath: "package.json" },
|
|
158
|
+
{ contents: renderBuildMjs(), relativePath: "build.mjs" },
|
|
159
|
+
{ contents: renderTsconfig(), relativePath: "tsconfig.json" },
|
|
160
|
+
{ contents: renderGitignore(), relativePath: ".gitignore" },
|
|
161
|
+
{ contents: renderReadme(name), relativePath: "README.md" },
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
// Write the scaffold to disk. Refuses to overwrite an existing
|
|
165
|
+
// directory: if `outDir` exists the function throws and leaves the
|
|
166
|
+
// filesystem untouched. On any write error after creating the
|
|
167
|
+
// directory, the partially-written tree is cleaned up so re-runs
|
|
168
|
+
// see a clean slate. Exported for unit testing.
|
|
169
|
+
export function writeScaffold(params) {
|
|
170
|
+
if (!isValidFunctionName(params.name)) {
|
|
171
|
+
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 });
|
|
172
|
+
}
|
|
173
|
+
const files = scaffoldFiles(params.name);
|
|
174
|
+
const written = [];
|
|
175
|
+
// Create the target directory with recursive: false so the check
|
|
176
|
+
// and the create happen in one syscall. mkdirSync throws EEXIST
|
|
177
|
+
// atomically if the path already exists, which closes the TOCTOU
|
|
178
|
+
// window between a separate existsSync check and the mkdir call.
|
|
179
|
+
try {
|
|
180
|
+
mkdirSync(params.outDir, { recursive: false });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const code = error.code;
|
|
184
|
+
if (code === "EEXIST") {
|
|
185
|
+
throw new Errors.CLIError(`Target directory already exists: ${params.outDir}. Refusing to overwrite. Remove it or pick a different --out-dir.`, { exit: 1 });
|
|
186
|
+
}
|
|
187
|
+
if (code === "ENOENT") {
|
|
188
|
+
throw new Errors.CLIError(`Parent directory does not exist for ${params.outDir}. Create it first or pick a different --out-dir.`, { exit: 1 });
|
|
189
|
+
}
|
|
190
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
191
|
+
throw new Errors.CLIError(`Failed to create ${params.outDir}: ${detail}`, {
|
|
192
|
+
exit: 1,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
const fullPath = resolve(params.outDir, file.relativePath);
|
|
198
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
199
|
+
writeFileSync(fullPath, file.contents, "utf8");
|
|
200
|
+
written.push(fullPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
// Roll back the partial scaffold so the user can retry without
|
|
205
|
+
// tripping the "directory already exists" guard above.
|
|
206
|
+
try {
|
|
207
|
+
rmSync(params.outDir, { force: true, recursive: true });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Best-effort cleanup; surface the original error regardless.
|
|
211
|
+
}
|
|
212
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
213
|
+
throw new Errors.CLIError(`Failed to write scaffold to ${params.outDir}: ${detail}`, { exit: 1 });
|
|
214
|
+
}
|
|
215
|
+
return { written };
|
|
216
|
+
}
|
|
217
|
+
class FunctionsInitCommand extends Command {
|
|
218
|
+
static description = `Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.
|
|
219
|
+
|
|
220
|
+
The scaffolded handler imports \`createPrimitiveClient\` from
|
|
221
|
+
\`@primitivedotdev/sdk/api\` and demonstrates the canonical pattern:
|
|
222
|
+
parse the email.received event, send a reply via the SDK, return a
|
|
223
|
+
JSON envelope. The build script uses esbuild's JS API and emits
|
|
224
|
+
./dist/handler.js, ready to hand to \`primitive functions:deploy --file\`.
|
|
225
|
+
|
|
226
|
+
Refuses to overwrite an existing directory. Use --out-dir to pick a
|
|
227
|
+
different target path than ./<name>/.`;
|
|
228
|
+
static summary = "Scaffold a new Primitive Function project ready for functions:deploy";
|
|
229
|
+
static examples = [
|
|
230
|
+
"<%= config.bin %> functions:init my-fn",
|
|
231
|
+
"<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn",
|
|
232
|
+
];
|
|
233
|
+
static args = {
|
|
234
|
+
name: Args.string({
|
|
235
|
+
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.",
|
|
236
|
+
required: true,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
static flags = {
|
|
240
|
+
"out-dir": Flags.string({
|
|
241
|
+
description: "Directory to scaffold into. Defaults to ./<name>/. Must not already exist.",
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
async run() {
|
|
245
|
+
const { args, flags } = await this.parse(FunctionsInitCommand);
|
|
246
|
+
const outDir = resolve(flags["out-dir"] ?? `./${args.name}`);
|
|
247
|
+
writeScaffold({ name: args.name, outDir });
|
|
248
|
+
this.log(`Scaffolded ${outDir}.`);
|
|
249
|
+
this.log("Next:");
|
|
250
|
+
this.log(` cd ${outDir}`);
|
|
251
|
+
this.log(" npm install");
|
|
252
|
+
this.log(" npm run build");
|
|
253
|
+
this.log(` primitive functions:deploy --name ${args.name} --file ./dist/handler.js`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
export default FunctionsInitCommand;
|
|
@@ -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({
|
package/dist/oclif/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import EmailsLatestCommand from "./commands/emails-latest.js";
|
|
|
5
5
|
import EmailsWaitCommand from "./commands/emails-wait.js";
|
|
6
6
|
import EmailsWatchCommand from "./commands/emails-watch.js";
|
|
7
7
|
import FunctionsDeployCommand from "./commands/functions-deploy.js";
|
|
8
|
+
import FunctionsInitCommand from "./commands/functions-init.js";
|
|
8
9
|
import FunctionsRedeployCommand from "./commands/functions-redeploy.js";
|
|
9
10
|
import FunctionsSetSecretCommand from "./commands/functions-set-secret.js";
|
|
10
11
|
import LoginCommand from "./commands/login.js";
|
|
@@ -140,6 +141,13 @@ export const COMMANDS = {
|
|
|
140
141
|
// inbound mail. `watch` defaults to a human table; `wait` defaults to JSONL.
|
|
141
142
|
"emails:watch": EmailsWatchCommand,
|
|
142
143
|
"emails:wait": EmailsWaitCommand,
|
|
144
|
+
// `functions:init` scaffolds a deployable Function project so a
|
|
145
|
+
// new author can go zero-to-deployed without writing the handler,
|
|
146
|
+
// package.json, build script, and tsconfig from scratch. The
|
|
147
|
+
// scaffolded handler imports from @primitivedotdev/sdk/api (the
|
|
148
|
+
// runtime-client subpath) and demonstrates client.send() so the
|
|
149
|
+
// first thing the author sees is the SDK pattern, not raw fetch.
|
|
150
|
+
"functions:init": FunctionsInitCommand,
|
|
143
151
|
// `functions:deploy` and `functions:redeploy` are file-input
|
|
144
152
|
// shortcuts for create-function / update-function. The underlying
|
|
145
153
|
// ops take `code` as a body string, which is awkward at the CLI
|
|
@@ -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
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -709,6 +709,39 @@
|
|
|
709
709
|
"summary": "Wait for matching inbound emails",
|
|
710
710
|
"enableJsonFlag": false
|
|
711
711
|
},
|
|
712
|
+
"functions:init": {
|
|
713
|
+
"aliases": [],
|
|
714
|
+
"args": {
|
|
715
|
+
"name": {
|
|
716
|
+
"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.",
|
|
717
|
+
"name": "name",
|
|
718
|
+
"required": true
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
"description": "Scaffold a new Primitive Function project in ./<name>/ with handler.ts, package.json, build.mjs, tsconfig.json, .gitignore, and README.md.\n\n The scaffolded handler imports `createPrimitiveClient` from\n `@primitivedotdev/sdk/api` and demonstrates the canonical pattern:\n parse the email.received event, send a reply via the SDK, return a\n JSON envelope. The build script uses esbuild's JS API and emits\n ./dist/handler.js, ready to hand to `primitive functions:deploy --file`.\n\n Refuses to overwrite an existing directory. Use --out-dir to pick a\n different target path than ./<name>/.",
|
|
722
|
+
"examples": [
|
|
723
|
+
"<%= config.bin %> functions:init my-fn",
|
|
724
|
+
"<%= config.bin %> functions:init my-fn --out-dir ./functions/my-fn"
|
|
725
|
+
],
|
|
726
|
+
"flags": {
|
|
727
|
+
"out-dir": {
|
|
728
|
+
"description": "Directory to scaffold into. Defaults to ./<name>/. Must not already exist.",
|
|
729
|
+
"name": "out-dir",
|
|
730
|
+
"hasDynamicHelp": false,
|
|
731
|
+
"multiple": false,
|
|
732
|
+
"type": "option"
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
"hasDynamicHelp": false,
|
|
736
|
+
"hiddenAliases": [],
|
|
737
|
+
"id": "functions:init",
|
|
738
|
+
"pluginAlias": "@primitivedotdev/sdk",
|
|
739
|
+
"pluginName": "@primitivedotdev/sdk",
|
|
740
|
+
"pluginType": "core",
|
|
741
|
+
"strict": true,
|
|
742
|
+
"summary": "Scaffold a new Primitive Function project ready for functions:deploy",
|
|
743
|
+
"enableJsonFlag": false
|
|
744
|
+
},
|
|
712
745
|
"functions:deploy": {
|
|
713
746
|
"aliases": [],
|
|
714
747
|
"args": {},
|
|
@@ -4250,5 +4283,5 @@
|
|
|
4250
4283
|
"enableJsonFlag": false
|
|
4251
4284
|
}
|
|
4252
4285
|
},
|
|
4253
|
-
"version": "0.
|
|
4286
|
+
"version": "0.24.0"
|
|
4254
4287
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primitivedotdev/sdk",
|
|
3
|
-
"version": "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",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"description": "View and replay webhook delivery attempts"
|
|
85
85
|
},
|
|
86
86
|
"functions": {
|
|
87
|
-
"description": "Deploy JavaScript handlers that run on inbound mail. Use `primitive functions:deploy --name <name> --file <bundle.js>` to create, `primitive functions:redeploy --id <id> --file <bundle.js>` to push a new bundle, and `primitive functions:set-secret --id <id> --key <KEY> --value <value> [--redeploy]` to write a secret (with optional one-call redeploy so the value lands in the running handler). The auto-generated functions:create-function / functions:update-function / functions:create-function-secret / functions:set-function-secret operations stay available for the full body-string surface."
|
|
87
|
+
"description": "Deploy JavaScript handlers that run on inbound mail. Use `primitive functions:init <name>` to scaffold a deployable project (handler, package.json, build script). Use `primitive functions:deploy --name <name> --file <bundle.js>` to create, `primitive functions:redeploy --id <id> --file <bundle.js>` to push a new bundle, and `primitive functions:set-secret --id <id> --key <KEY> --value <value> [--redeploy]` to write a secret (with optional one-call redeploy so the value lands in the running handler). The auto-generated functions:create-function / functions:update-function / functions:create-function-secret / functions:set-function-secret operations stay available for the full body-string surface."
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
90
|
"topicSeparator": " "
|