@sparelabs/sightline-extension-cli 0.1.3 → 0.1.5
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 +25 -3
- package/dist/args.js +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +47 -29
- package/dist/core-api.d.ts +41 -0
- package/dist/core-api.js +15 -0
- package/dist/deploy.d.ts +31 -1
- package/dist/deploy.js +79 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/local-run.d.ts +30 -0
- package/dist/local-run.js +115 -0
- package/dist/project.d.ts +50 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ You need Node 20.19+, [Deno](https://deno.com) (the extension runtime) and Docke
|
|
|
21
21
|
| `deploy [--bindings bindings.json] [--no-build]` | Builds, publishes the release to a workspace (identical bytes are a no-op) and installs it. Refuses when another version is installed: use `upgrade`. |
|
|
22
22
|
| `upgrade [--bindings bindings.json] [--no-build]` | Builds, publishes, and moves the installed extension to the manifest's version. A permission the new version adds needs a binding. |
|
|
23
23
|
| `uninstall [<id>] [--purge --yes]` | Uninstalls. Data is kept 30 days; `--purge --yes` drops it now. |
|
|
24
|
+
| `yank <version> [--reason "…"] [--id <ext>]` | Withdraws a published release: it can no longer be installed, upgraded or rolled back to. An installation running it keeps running, flagged (the CLI warns) until you upgrade it. Idempotent; there is no un-yank. A deploy token needs the `yank` action. |
|
|
25
|
+
| `run <job> [--no-wait] [--timeout <s>] [--id <ext>]` | Starts a run of the job now, outside its schedule, then reads its status every 2 s until it finishes (a job that continues makes several calls under one run), printing each call (trigger, status, HTTP status, duration, error) and the result. Exit 1 unless it `succeeded`. `--no-wait` returns once it started; past `--timeout` (default 900 s) the CLI stops waiting, the run goes on. A deploy token needs the `run_job` action. |
|
|
24
26
|
| `webhooks send <name> --body <file> [--url <webhook URL>] [--secret-env VAR] [--content-type …] [--timestamp <unix s>]` | Signs the file's raw bytes the way Sightline verifies the manifest webhook `<name>` (HMAC-SHA256 hex after its `prefix`, over `<timestamp>.<raw body>` when it declares a `timestampHeader`) and POSTs them. Without `--url` it goes to the running `dev`, whose emulator verifies it like Sightline (401 with the reason when it fails, 409 for a replay) and forwards it to your `/hooks/<name>`; with `--url` (the webhook URL from the workspace's Setup) to a real workspace. The secret comes from `SL_SECRET_<NAME>` or `--secret-env VAR`, never from a flag; locally `devSecrets` win over the environment, as `dev` injects them, so the delivery is signed with the value the emulator verifies with. Exit 1 on a non-2xx answer. |
|
|
25
27
|
|
|
26
28
|
Common options: `--dir <project>`, `--manifest <file>`, `--config <file>`, `--entry <file>`.
|
|
@@ -33,16 +35,36 @@ Local only; never deployed.
|
|
|
33
35
|
{
|
|
34
36
|
"entry": "server/main.ts",
|
|
35
37
|
"contract": {
|
|
36
|
-
"tool": { "name": "notes_list", "input": { "limit": 5 }, "as": "user:alice" },
|
|
38
|
+
"tool": { "name": "notes_list", "input": { "limit": 5 }, "as": "user:alice", "grants": ["my-notes.read"] },
|
|
39
|
+
"permissions": [
|
|
40
|
+
{ "tool": "notes_add", "input": { "text": "hi" }, "grants": ["my-notes.write"] },
|
|
41
|
+
{ "api": "GET /notes", "grants": ["my-notes.read"] }
|
|
42
|
+
],
|
|
37
43
|
"job": { "name": "weekly_digest" },
|
|
38
44
|
"hook": { "name": "inbound", "body": { "event": "ping" } }
|
|
39
45
|
},
|
|
40
46
|
"settings": { "greeting": "hello" },
|
|
41
|
-
"devSecrets": { "inbound_hook": "dev-only-value" }
|
|
47
|
+
"devSecrets": { "inbound_hook": "dev-only-value" },
|
|
48
|
+
"users": [{ "name": "alice", "email": "alice@example.test", "grants": ["my-notes.read", "my-notes.write"] }, { "name": "eve" }],
|
|
49
|
+
"workspace": { "name": "Acme", "timezone": "Europe/London", "locale": "en-GB", "weekStart": 1 },
|
|
50
|
+
"people": [{ "displayName": "Ada Lovelace", "title": "Engineer", "department": "R&D" }],
|
|
51
|
+
"coreScopes": ["core:people:read"]
|
|
42
52
|
}
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
| Field | `dev` and `test` |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `contract.tool` | The tool `test` calls (default: the manifest's first, as `user:alice` with `{}`); `grants`: what the caller holds for it. |
|
|
58
|
+
| `contract.permissions` | Each permission-checked tool or `/api` route (`"<METHOD> /path"`) must succeed holding `grants` and be refused holding nothing (`permission_denied` / `scope_missing`, or 403). Called as `as`, default `user:contract-tester`. |
|
|
59
|
+
| `contract.job`, `contract.hook` | A job and a hook `test` routes as the installation. |
|
|
60
|
+
| `settings` | What `GET /v1/installation/settings` answers. |
|
|
61
|
+
| `devSecrets` | Injected as `SL_SECRET_<NAME>` (over the same variables from your environment); the emulator verifies signed webhooks with them. |
|
|
62
|
+
| `users` | The emulated people: their grants are their tokens' scopes, `access` their bits on your resources. Default: alice and bob, holding nothing. |
|
|
63
|
+
| `workspace` | What `/v1/me` reports (default "Dev workspace", UTC, en-US, Sunday). |
|
|
64
|
+
| `people` | The people directory (`/v1/people`, `/v1/people/:id`): `displayName`, optional `id` (a uuid; default derived from the name, so `alice` is `user:alice`), `avatarUrl`, `title`, `department`, `active`. Default: one person per user. |
|
|
65
|
+
| `coreScopes` | The core scopes the installation holds (default: the manifest's `scopes.required`, which installing grants). The directory needs `core:people:read`. |
|
|
66
|
+
|
|
67
|
+
The emulator also gets the manifest, so its webhooks are verified like Sightline's.
|
|
46
68
|
|
|
47
69
|
## Deploying
|
|
48
70
|
|
package/dist/args.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Argument parsing for sightline-ext: positionals, `--flag value`, `--flag=value`
|
|
2
2
|
// and bare `--flag` (true). No dependencies, so it runs under Node and Deno alike.
|
|
3
3
|
/** Flags that never take a value, so `--purge my-ext` keeps `my-ext` positional. */
|
|
4
|
-
const BOOLEAN_FLAGS = new Set(["purge", "yes", "force", "no-db", "no-build", "help", "json", "ui", "watch"]);
|
|
4
|
+
const BOOLEAN_FLAGS = new Set(["purge", "yes", "force", "no-db", "no-build", "no-wait", "keep-db", "help", "json", "ui", "watch"]);
|
|
5
5
|
export function parseArgs(argv) {
|
|
6
6
|
const positional = [];
|
|
7
7
|
const flags = {};
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const USAGE = "sightline-ext <command> [options]\n\n init <dir> Scaffold a new extension repository\n [--id <kebab-id>] [--name \"Display name\"] [--publisher <kebab-id>] [--force]\n build Validate the manifest and migrations, bundle server/main.ts into dist/server.mjs\n dev Build, then run the extension against an emulated Sightline, rebuilding on change\n [--port <n>] [--core-port 8787] [--no-db]\n test Build, run the extension against the emulator, and check the HTTP contract\n [--no-build] [--no-db] [--keep-db]\n deploy Build, publish the release and install it in a workspace\n upgrade Build, publish the release and move the installed extension to it\n (deploy, upgrade: [--bindings bindings.json] [--no-build])\n uninstall Remove the extension from the workspace (its data is kept 30 days)\n [--purge --yes]\n webhooks send <name> --body <file>\n Sign the file's raw bytes the way Sightline verifies them and POST them: to the\n running `dev` (verified and forwarded to your /hooks/<name>), or with\n --url <webhook URL from Setup> to a workspace. The secret is read from\n SL_SECRET_<NAME> (or --secret-env VAR, or devSecrets), never from a flag.\n [--url <url>] [--secret-env VAR] [--content-type application/json] [--timestamp <unix s>]\n\n Common: [--dir <project>] [--manifest sightline.extension.json] [--config sightline-ext.json]\n deploy, upgrade, uninstall: [--url https://<workspace>] [--api-url <core API base>]\n\nDeploying needs the workspace and a deploy token issued by one of its admins:\n SIGHTLINE_URL https://<your workspace> (or SIGHTLINE_API_URL, the full core API base)\n SIGHTLINE_DEPLOY_TOKEN the token (only ever read from the environment)\n SIGHTLINE_API_KEY the gateway's public api key, when your workspace needs one";
|
|
1
|
+
export declare const USAGE = "sightline-ext <command> [options]\n\n init <dir> Scaffold a new extension repository\n [--id <kebab-id>] [--name \"Display name\"] [--publisher <kebab-id>] [--force]\n build Validate the manifest and migrations, bundle server/main.ts into dist/server.mjs\n dev Build, then run the extension against an emulated Sightline, rebuilding on change\n [--port <n>] [--core-port 8787] [--no-db]\n test Build, run the extension against the emulator, and check the HTTP contract\n [--no-build] [--no-db] [--keep-db]\n deploy Build, publish the release and install it in a workspace\n upgrade Build, publish the release and move the installed extension to it\n (deploy, upgrade: [--bindings bindings.json] [--no-build])\n uninstall Remove the extension from the workspace (its data is kept 30 days)\n [--purge --yes]\n yank <version> Withdraw a published release: it can no longer be installed, upgraded or rolled\n back to (an installation running it keeps running, flagged). No un-yank.\n [--reason \"\u2026\"]\n run <job> Start a run of a job now, outside its schedule, wait for it and print each call\n and the result; exit 1 unless it succeeded. [--no-wait] [--timeout <seconds, default 900>]\n webhooks send <name> --body <file>\n Sign the file's raw bytes the way Sightline verifies them and POST them: to the\n running `dev` (verified and forwarded to your /hooks/<name>), or with\n --url <webhook URL from Setup> to a workspace. The secret is read from\n SL_SECRET_<NAME> (or --secret-env VAR, or devSecrets), never from a flag.\n [--url <url>] [--secret-env VAR] [--content-type application/json] [--timestamp <unix s>]\n\n Common: [--dir <project>] [--manifest sightline.extension.json] [--config sightline-ext.json]\n deploy, upgrade, uninstall, yank, run: [--url https://<workspace>] [--api-url <core API base>]\n yank, run: on the project's extension (the manifest's id), or --id <extension id> with no project.\n A deploy token needs the action: publish, install, upgrade, uninstall, yank or run_job.\n\nDeploying needs the workspace and a deploy token issued by one of its admins:\n SIGHTLINE_URL https://<your workspace> (or SIGHTLINE_API_URL, the full core API base)\n SIGHTLINE_DEPLOY_TOKEN the token (only ever read from the environment)\n SIGHTLINE_API_KEY the gateway's public api key, when your workspace needs one";
|
|
2
2
|
export type Out = (line: string) => void;
|
|
3
3
|
export declare function packageVersion(): string;
|
|
4
4
|
export declare function main(argv: string[], out?: Out): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,9 @@ import { assertContract, formatReport, runContractTests } from "@sparelabs/sight
|
|
|
8
8
|
import { bool, int, parseArgs, str } from "./args.js";
|
|
9
9
|
import { build, bundleOptions } from "./build.js";
|
|
10
10
|
import { CoreApiError, createCoreApi, describeError } from "./core-api.js";
|
|
11
|
-
import { deploy, parseBindings, uninstall, upgrade } from "./deploy.js";
|
|
12
|
-
import {
|
|
11
|
+
import { deploy, parseBindings, runJob, uninstall, upgrade, yank } from "./deploy.js";
|
|
12
|
+
import { contractConfig, emulatorConfig, secretEnv } from "./local-run.js";
|
|
13
|
+
import { loadProject } from "./project.js";
|
|
13
14
|
import { templateFiles } from "./template.js";
|
|
14
15
|
import { planWebhookSend } from "./webhooks.js";
|
|
15
16
|
export const USAGE = `sightline-ext <command> [options]
|
|
@@ -26,6 +27,11 @@ export const USAGE = `sightline-ext <command> [options]
|
|
|
26
27
|
(deploy, upgrade: [--bindings bindings.json] [--no-build])
|
|
27
28
|
uninstall Remove the extension from the workspace (its data is kept 30 days)
|
|
28
29
|
[--purge --yes]
|
|
30
|
+
yank <version> Withdraw a published release: it can no longer be installed, upgraded or rolled
|
|
31
|
+
back to (an installation running it keeps running, flagged). No un-yank.
|
|
32
|
+
[--reason "…"]
|
|
33
|
+
run <job> Start a run of a job now, outside its schedule, wait for it and print each call
|
|
34
|
+
and the result; exit 1 unless it succeeded. [--no-wait] [--timeout <seconds, default 900>]
|
|
29
35
|
webhooks send <name> --body <file>
|
|
30
36
|
Sign the file's raw bytes the way Sightline verifies them and POST them: to the
|
|
31
37
|
running \`dev\` (verified and forwarded to your /hooks/<name>), or with
|
|
@@ -34,7 +40,9 @@ export const USAGE = `sightline-ext <command> [options]
|
|
|
34
40
|
[--url <url>] [--secret-env VAR] [--content-type application/json] [--timestamp <unix s>]
|
|
35
41
|
|
|
36
42
|
Common: [--dir <project>] [--manifest sightline.extension.json] [--config sightline-ext.json]
|
|
37
|
-
deploy, upgrade, uninstall: [--url https://<workspace>] [--api-url <core API base>]
|
|
43
|
+
deploy, upgrade, uninstall, yank, run: [--url https://<workspace>] [--api-url <core API base>]
|
|
44
|
+
yank, run: on the project's extension (the manifest's id), or --id <extension id> with no project.
|
|
45
|
+
A deploy token needs the action: publish, install, upgrade, uninstall, yank or run_job.
|
|
38
46
|
|
|
39
47
|
Deploying needs the workspace and a deploy token issued by one of its admins:
|
|
40
48
|
SIGHTLINE_URL https://<your workspace> (or SIGHTLINE_API_URL, the full core API base)
|
|
@@ -62,18 +70,6 @@ async function buildProject(p, out) {
|
|
|
62
70
|
out(` warning: ${w}`);
|
|
63
71
|
return result;
|
|
64
72
|
}
|
|
65
|
-
function secretEnv(p) {
|
|
66
|
-
const env = {};
|
|
67
|
-
for (const [k, v] of Object.entries(process.env))
|
|
68
|
-
if (k.startsWith("SL_SECRET_") && v !== undefined)
|
|
69
|
-
env[k] = v;
|
|
70
|
-
for (const [name, value] of Object.entries(p.config.devSecrets ?? {})) {
|
|
71
|
-
if (!/^[a-z][a-z0-9_-]{0,63}$/i.test(name) || typeof value !== "string")
|
|
72
|
-
throw new Error(`devSecrets: "${name}" must be a secret name with a string value`);
|
|
73
|
-
env[`SL_SECRET_${name.toUpperCase().replace(/-/g, "_")}`] = value;
|
|
74
|
-
}
|
|
75
|
-
return env;
|
|
76
|
-
}
|
|
77
73
|
const hasMigrations = (p) => existsSync(p.migrationsDir) && readdirSync(p.migrationsDir).some((f) => f.endsWith(".sql"));
|
|
78
74
|
function waitForSignal() {
|
|
79
75
|
return new Promise((done) => {
|
|
@@ -106,14 +102,9 @@ function init(positional, flags, out) {
|
|
|
106
102
|
async function dev(flags, out) {
|
|
107
103
|
const p = project(flags);
|
|
108
104
|
await buildProject(p, out);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
settings: p.config.settings ?? {},
|
|
113
|
-
// Its ext-hooks route verifies webhooks with these, as core does with the installation's secrets.
|
|
114
|
-
manifest: p.manifest,
|
|
115
|
-
secrets: secretEnv(p),
|
|
116
|
-
});
|
|
105
|
+
// sightline-ext.json's users, workspace, people and coreScopes; the manifest (its webhooks, its
|
|
106
|
+
// required core scopes); devSecrets, which its ext-hooks route verifies with as core does.
|
|
107
|
+
const emulator = await createCoreEmulator(emulatorConfig(p, secretEnv(p, process.env)));
|
|
117
108
|
const core = await listen(emulator.handler, { port: int(flags, "core-port") ?? 8787 });
|
|
118
109
|
emulator.setOrigin(core.url);
|
|
119
110
|
const env = emulator.runtimeEnv();
|
|
@@ -181,11 +172,12 @@ async function test(flags, out) {
|
|
|
181
172
|
await buildProject(p, out);
|
|
182
173
|
else if (!existsSync(p.outfile))
|
|
183
174
|
throw new Error(`--no-build, but ${p.outfile} does not exist`);
|
|
184
|
-
|
|
185
|
-
const
|
|
175
|
+
// Read and checked before anything starts: a config mistake costs no container.
|
|
176
|
+
const contract = contractConfig(p);
|
|
177
|
+
const emulator = await createCoreEmulator(emulatorConfig(p, secretEnv(p, process.env)));
|
|
186
178
|
const core = await listen(emulator.handler);
|
|
187
179
|
emulator.setOrigin(core.url);
|
|
188
|
-
const env = emulator.runtimeEnv(
|
|
180
|
+
const env = emulator.runtimeEnv();
|
|
189
181
|
let db = null;
|
|
190
182
|
let ext = null;
|
|
191
183
|
try {
|
|
@@ -197,9 +189,7 @@ async function test(flags, out) {
|
|
|
197
189
|
ext = await runExtension({ bundle: p.outfile, env, port: await freePort() });
|
|
198
190
|
const report = await runContractTests(ext.url, {
|
|
199
191
|
coreUrl: core.url,
|
|
200
|
-
|
|
201
|
-
job: contract.job,
|
|
202
|
-
hook: contract.hook,
|
|
192
|
+
...contract,
|
|
203
193
|
expect: { version: p.version },
|
|
204
194
|
});
|
|
205
195
|
out(formatReport(report));
|
|
@@ -256,6 +246,13 @@ function coreApiFromEnv(flags, env = process.env) {
|
|
|
256
246
|
const apiUrl = str(flags, "api-url") ?? (str(flags, "url") ? undefined : env.SIGHTLINE_API_URL);
|
|
257
247
|
return createCoreApi({ url, apiUrl, token, apiKey: env.SIGHTLINE_API_KEY });
|
|
258
248
|
}
|
|
249
|
+
/** `--id <extension>`, else the project's manifest id. */
|
|
250
|
+
function extensionId(flags) {
|
|
251
|
+
const id = str(flags, "id") ?? project(flags).id;
|
|
252
|
+
if (!id)
|
|
253
|
+
throw new Error("name the extension: --id <extension id>, or run in its project (the manifest's id)");
|
|
254
|
+
return id;
|
|
255
|
+
}
|
|
259
256
|
function readBindings(flags) {
|
|
260
257
|
const file = str(flags, "bindings");
|
|
261
258
|
if (!file)
|
|
@@ -312,6 +309,27 @@ export async function main(argv, out = console.log) {
|
|
|
312
309
|
await uninstall(api, id, { purge }, out);
|
|
313
310
|
return 0;
|
|
314
311
|
}
|
|
312
|
+
case "yank": {
|
|
313
|
+
const version = rest[0];
|
|
314
|
+
if (!version)
|
|
315
|
+
throw new Error('usage: sightline-ext yank <version> [--reason "…"]');
|
|
316
|
+
const api = coreApiFromEnv(flags);
|
|
317
|
+
await yank(api, extensionId(flags), version, { reason: str(flags, "reason") }, out);
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
case "run": {
|
|
321
|
+
const job = rest[0];
|
|
322
|
+
if (!job)
|
|
323
|
+
throw new Error("usage: sightline-ext run <job> [--no-wait] [--timeout <seconds>]");
|
|
324
|
+
const timeout = str(flags, "timeout");
|
|
325
|
+
const seconds = timeout === undefined ? undefined : Number(timeout);
|
|
326
|
+
if (seconds !== undefined && (!Number.isInteger(seconds) || seconds < 1))
|
|
327
|
+
throw new Error("--timeout is a whole number of seconds");
|
|
328
|
+
const api = coreApiFromEnv(flags);
|
|
329
|
+
const wait = !bool(flags, "no-wait");
|
|
330
|
+
const run = await runJob(api, extensionId(flags), job, { wait, timeoutMs: seconds === undefined ? undefined : seconds * 1000 }, out);
|
|
331
|
+
return !wait || run.status === "succeeded" ? 0 : 1;
|
|
332
|
+
}
|
|
315
333
|
case "webhooks":
|
|
316
334
|
return await webhooks(rest, flags, out);
|
|
317
335
|
case "version":
|
package/dist/core-api.d.ts
CHANGED
|
@@ -39,6 +39,36 @@ export interface InstallationView {
|
|
|
39
39
|
lastError?: string | null;
|
|
40
40
|
[key: string]: unknown;
|
|
41
41
|
}
|
|
42
|
+
/** One call of a job run (core's `runView`). */
|
|
43
|
+
export interface JobCallView {
|
|
44
|
+
id: string;
|
|
45
|
+
seq: number;
|
|
46
|
+
attempt: number;
|
|
47
|
+
trigger: string;
|
|
48
|
+
status: string;
|
|
49
|
+
startedAt?: string | null;
|
|
50
|
+
finishedAt?: string | null;
|
|
51
|
+
durationMs?: number | null;
|
|
52
|
+
httpStatus?: number | null;
|
|
53
|
+
error?: string | null;
|
|
54
|
+
retryAt?: string | null;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}
|
|
57
|
+
/** A job run: 202 from run-now (`status: "running"`), or the run lookup with its calls. */
|
|
58
|
+
export interface JobRunView {
|
|
59
|
+
runId: string;
|
|
60
|
+
job: string;
|
|
61
|
+
version: string;
|
|
62
|
+
/** running, succeeded, failed, timed_out or abandoned. */
|
|
63
|
+
status: string;
|
|
64
|
+
trigger?: string;
|
|
65
|
+
startedAt?: string | null;
|
|
66
|
+
finishedAt?: string | null;
|
|
67
|
+
/** Between calls: when the next continuation or retry is due. */
|
|
68
|
+
resumeAt?: string | null;
|
|
69
|
+
calls?: JobCallView[];
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
42
72
|
export interface PublishInput {
|
|
43
73
|
manifest: Record<string, unknown>;
|
|
44
74
|
bundle: Uint8Array;
|
|
@@ -86,6 +116,17 @@ export interface CoreApi {
|
|
|
86
116
|
installation: InstallationView;
|
|
87
117
|
warnings: string[];
|
|
88
118
|
}>;
|
|
119
|
+
/** Withdraw a published release (a deploy token needs the `yank` action). */
|
|
120
|
+
yank(id: string, version: string, body: {
|
|
121
|
+
reason?: string;
|
|
122
|
+
}): Promise<{
|
|
123
|
+
release: ReleaseView;
|
|
124
|
+
installed: boolean;
|
|
125
|
+
}>;
|
|
126
|
+
/** Start a run of a job now (a deploy token needs `run_job`): 202 `{ run }`. */
|
|
127
|
+
runJob(id: string, job: string): Promise<JobRunView>;
|
|
128
|
+
/** A run's state and calls (`run_job`). */
|
|
129
|
+
jobRun(id: string, job: string, runId: string): Promise<JobRunView>;
|
|
89
130
|
}
|
|
90
131
|
/** A deploy token an admin issued (`slxd_…`), as opposed to an admin's own session token. */
|
|
91
132
|
export declare function isDeployToken(token: string): boolean;
|
package/dist/core-api.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// POST /core/v1/extensions/:id/install { version, bindings? }
|
|
7
7
|
// POST /core/v1/extensions/:id/upgrade { version, bindings? }
|
|
8
8
|
// POST /core/v1/extensions/:id/uninstall { purge? }
|
|
9
|
+
// POST /core/v1/extensions/:id/releases/:version/yank { reason? } → { release, installed }
|
|
10
|
+
// POST /core/v1/extensions/:id/jobs/:name/run 202 { run }
|
|
11
|
+
// GET /core/v1/extensions/:id/jobs/:name/runs/:runId { run } (status and calls)
|
|
9
12
|
//
|
|
10
13
|
// Auth is a bearer token from SIGHTLINE_DEPLOY_TOKEN: a deploy token a tenant
|
|
11
14
|
// admin issued (`slxd_…`, sent to the same routes under /core/v1/deploy, the
|
|
@@ -69,6 +72,8 @@ export function describeError(err) {
|
|
|
69
72
|
const errors = (d.errors ?? []);
|
|
70
73
|
return `${err.message}${errors.map((e) => `\n ${e.path ?? ""}: ${e.message ?? ""}`).join("")}`;
|
|
71
74
|
}
|
|
75
|
+
case "job_running":
|
|
76
|
+
return `${err.message}${typeof d.runId === "string" ? `\n the run in progress: ${d.runId}` : ""}`;
|
|
72
77
|
case "bundle_rejected":
|
|
73
78
|
case "ui_rejected": {
|
|
74
79
|
const errors = (d.errors ?? []);
|
|
@@ -162,5 +167,15 @@ export function createCoreApi(options) {
|
|
|
162
167
|
const r = await call("POST", `/extensions/${encodeURIComponent(id)}/uninstall`, body);
|
|
163
168
|
return { installation: r.installation, warnings: r.warnings ?? [] };
|
|
164
169
|
},
|
|
170
|
+
async yank(id, version, body) {
|
|
171
|
+
const r = await call("POST", `/extensions/${encodeURIComponent(id)}/releases/${encodeURIComponent(version)}/yank`, body);
|
|
172
|
+
return { release: r.release, installed: r.installed === true };
|
|
173
|
+
},
|
|
174
|
+
async runJob(id, job) {
|
|
175
|
+
return (await call("POST", `/extensions/${encodeURIComponent(id)}/jobs/${encodeURIComponent(job)}/run`, {})).run;
|
|
176
|
+
},
|
|
177
|
+
async jobRun(id, job, runId) {
|
|
178
|
+
return (await call("GET", `/extensions/${encodeURIComponent(id)}/jobs/${encodeURIComponent(job)}/runs/${encodeURIComponent(runId)}`)).run;
|
|
179
|
+
},
|
|
165
180
|
};
|
|
166
181
|
}
|
package/dist/deploy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CoreApi, type InstallationView, type JobRunView, type PublishInput, type ReleaseView } from "./core-api.ts";
|
|
2
2
|
export type Out = (line: string) => void;
|
|
3
3
|
export declare function deploy(api: CoreApi, release: PublishInput, options: {
|
|
4
4
|
bindings?: Record<string, string>;
|
|
@@ -11,3 +11,33 @@ export declare function uninstall(api: CoreApi, id: string, options: {
|
|
|
11
11
|
}, out: Out): Promise<InstallationView>;
|
|
12
12
|
/** --bindings <file>: { "<ext>.<permission>": "<core permission>" }. */
|
|
13
13
|
export declare function parseBindings(raw: unknown): Record<string, string>;
|
|
14
|
+
/** Core's cap on a yank reason. */
|
|
15
|
+
export declare const MAX_YANK_REASON = 500;
|
|
16
|
+
/**
|
|
17
|
+
* yank <version>: withdraw a published release. It can no longer be installed,
|
|
18
|
+
* upgraded or rolled back to; an installation running it keeps running, flagged,
|
|
19
|
+
* until it is upgraded. Idempotent (the first yank stands); there is no un-yank.
|
|
20
|
+
*/
|
|
21
|
+
export declare function yank(api: CoreApi, id: string, version: string, options: {
|
|
22
|
+
reason?: string;
|
|
23
|
+
}, out: Out): Promise<{
|
|
24
|
+
release: ReleaseView;
|
|
25
|
+
installed: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
export interface RunJobOptions {
|
|
28
|
+
/** false: start the run and return at once. */
|
|
29
|
+
wait?: boolean;
|
|
30
|
+
/** Between status reads. Default 2 s. */
|
|
31
|
+
pollMs?: number;
|
|
32
|
+
/** Give up waiting (the run goes on in the workspace). Default 15 min. */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
sleep?: (ms: number) => Promise<void>;
|
|
35
|
+
now?: () => number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* run <job>: start a run of a job now, outside its schedule, then read its
|
|
39
|
+
* status until it finishes (a job that continues makes several calls under one
|
|
40
|
+
* run id) and print each call and the result. The run happens in the
|
|
41
|
+
* workspace: giving up waiting does not stop it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runJob(api: CoreApi, id: string, job: string, options: RunJobOptions, out: Out): Promise<JobRunView>;
|
package/dist/deploy.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// deploy / upgrade / uninstall: the release lifecycle against a workspace's
|
|
2
|
+
// installer API. A deploy publishes the built release (idempotent for identical
|
|
3
|
+
// bytes) and installs it; an upgrade publishes and moves an existing
|
|
4
|
+
// installation to it; an uninstall removes it (data kept 30 days unless --purge).
|
|
5
|
+
// A yank withdraws a published release; a run starts a job now and waits for it.
|
|
6
|
+
import { CoreApiError } from "./core-api.js";
|
|
1
7
|
/** States in which an extension counts as installed (core keeps the row after uninstall). */
|
|
2
8
|
const LIVE = new Set(["enabled", "disabled", "provisioning"]);
|
|
3
9
|
async function current(api, id) {
|
|
@@ -69,3 +75,76 @@ export function parseBindings(raw) {
|
|
|
69
75
|
}
|
|
70
76
|
return out;
|
|
71
77
|
}
|
|
78
|
+
const RELEASE_VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
79
|
+
/** Core's cap on a yank reason. */
|
|
80
|
+
export const MAX_YANK_REASON = 500;
|
|
81
|
+
/**
|
|
82
|
+
* yank <version>: withdraw a published release. It can no longer be installed,
|
|
83
|
+
* upgraded or rolled back to; an installation running it keeps running, flagged,
|
|
84
|
+
* until it is upgraded. Idempotent (the first yank stands); there is no un-yank.
|
|
85
|
+
*/
|
|
86
|
+
export async function yank(api, id, version, options, out) {
|
|
87
|
+
if (!RELEASE_VERSION_RE.test(version))
|
|
88
|
+
throw new Error(`usage: sightline-ext yank <version> [--reason "…"]; "${version}" is not MAJOR.MINOR.PATCH`);
|
|
89
|
+
const reason = options.reason?.trim();
|
|
90
|
+
if (reason !== undefined && reason.length > MAX_YANK_REASON)
|
|
91
|
+
throw new Error(`--reason is at most ${MAX_YANK_REASON} characters`);
|
|
92
|
+
const result = await api.yank(id, version, reason ? { reason } : {});
|
|
93
|
+
out(`yanked ${id}@${version}${reason ? ` (${reason})` : ""}: it can no longer be installed, upgraded or rolled back to`);
|
|
94
|
+
if (result.installed)
|
|
95
|
+
out(` warning: the workspace runs ${id}@${version} now; it keeps running, flagged, until you \`sightline-ext upgrade\` to a release that is not yanked`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
/** Statuses a run ends in (core's run lookup). */
|
|
99
|
+
const FINISHED = new Set(["succeeded", "failed", "timed_out", "abandoned"]);
|
|
100
|
+
function describeCall(c) {
|
|
101
|
+
const parts = [`call ${c.seq}`, c.trigger, c.status];
|
|
102
|
+
if (typeof c.httpStatus === "number")
|
|
103
|
+
parts.push(`HTTP ${c.httpStatus}`);
|
|
104
|
+
if (typeof c.durationMs === "number")
|
|
105
|
+
parts.push(`${c.durationMs} ms`);
|
|
106
|
+
return ` ${parts.join(", ")}${c.error ? `: ${c.error}` : ""}${c.retryAt ? ` (retry at ${c.retryAt})` : ""}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* run <job>: start a run of a job now, outside its schedule, then read its
|
|
110
|
+
* status until it finishes (a job that continues makes several calls under one
|
|
111
|
+
* run id) and print each call and the result. The run happens in the
|
|
112
|
+
* workspace: giving up waiting does not stop it.
|
|
113
|
+
*/
|
|
114
|
+
export async function runJob(api, id, job, options, out) {
|
|
115
|
+
if (!job)
|
|
116
|
+
throw new Error("usage: sightline-ext run <job>");
|
|
117
|
+
const started = await api.runJob(id, job);
|
|
118
|
+
out(`started ${id} job ${job} (run ${started.runId}, version ${started.version})`);
|
|
119
|
+
if (options.wait === false)
|
|
120
|
+
return started;
|
|
121
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
122
|
+
const now = options.now ?? Date.now;
|
|
123
|
+
const timeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
124
|
+
const deadline = now() + timeoutMs;
|
|
125
|
+
let printed = 0;
|
|
126
|
+
let run = started;
|
|
127
|
+
for (;;) {
|
|
128
|
+
await sleep(options.pollMs ?? 2_000);
|
|
129
|
+
try {
|
|
130
|
+
run = await api.jobRun(id, job, started.runId);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// A run is claimed before its first call is readable everywhere: not found yet is not an answer.
|
|
134
|
+
if (!(e instanceof CoreApiError && e.code === "run_not_found"))
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
const done = (run.calls ?? []).filter((c) => c.status !== "running");
|
|
138
|
+
for (const c of done.slice(printed))
|
|
139
|
+
out(describeCall(c));
|
|
140
|
+
printed = Math.max(printed, done.length);
|
|
141
|
+
if (FINISHED.has(run.status))
|
|
142
|
+
break;
|
|
143
|
+
if (now() >= deadline) {
|
|
144
|
+
out(`still ${run.status} after ${Math.round(timeoutMs / 1000)} s; the run goes on in the workspace (run ${started.runId})`);
|
|
145
|
+
return run;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
out(`${job} ${run.status}${run.finishedAt ? ` at ${run.finishedAt}` : ""} (${(run.calls ?? []).length} call(s))`);
|
|
149
|
+
return run;
|
|
150
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { main, packageVersion, USAGE } from "./cli.ts";
|
|
2
2
|
export { bool, type Flags, parseArgs, type ParsedArgs, str } from "./args.ts";
|
|
3
3
|
export { build, type BuildDeps, type BuildResult, type Bundler, type BundleLinter, bundleOptions, type BundleOptions, collectUi, manifestProblems, type ManifestValidator, type UiValidator } from "./build.ts";
|
|
4
|
-
export { apiBase, type CoreApi, CoreApiError, type CoreApiOptions, createCoreApi, describeError, type InstallationView, isDeployToken, lifecycleBase, type PublishInput, type ReleaseView, sha256Hex } from "./core-api.ts";
|
|
5
|
-
export { deploy, parseBindings, uninstall, upgrade } from "./deploy.ts";
|
|
6
|
-
export {
|
|
4
|
+
export { apiBase, type CoreApi, CoreApiError, type CoreApiOptions, createCoreApi, describeError, type InstallationView, isDeployToken, type JobCallView, type JobRunView, lifecycleBase, type PublishInput, type ReleaseView, sha256Hex } from "./core-api.ts";
|
|
5
|
+
export { deploy, MAX_YANK_REASON, parseBindings, runJob, type RunJobOptions, uninstall, upgrade, yank } from "./deploy.ts";
|
|
6
|
+
export { contractConfig, type ContractRunConfig, type EmulatorConfig, emulatorConfig, secretEnv } from "./local-run.ts";
|
|
7
|
+
export { CONFIG_FILE, type ContractConfig, defaultContract, hostedRuleProblems, loadProject, MANIFEST_FILE, migrationProblems, type MigrationFile, type PermissionCheckConfig, type PersonConfig, type Project, type ProjectConfig, readMigrations, readUiFiles, type UiFileOnDisk, uiFrames, type UserConfig, type WorkspaceConfig, } from "./project.ts";
|
|
7
8
|
export { lintBundle, MAX_BUNDLE_BYTES } from "@sparelabs/sightline-extension-manifest";
|
|
8
9
|
export { templateFiles, type TemplateOptions, templateProblems, toolPrefix } from "./template.ts";
|
|
9
10
|
export { planWebhookSend, secretVar, type WebhookSendInput, type WebhookSendPlan } from "./webhooks.ts";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { main, packageVersion, USAGE } from "./cli.js";
|
|
|
5
5
|
export { bool, parseArgs, str } from "./args.js";
|
|
6
6
|
export { build, bundleOptions, collectUi, manifestProblems } from "./build.js";
|
|
7
7
|
export { apiBase, CoreApiError, createCoreApi, describeError, isDeployToken, lifecycleBase, sha256Hex } from "./core-api.js";
|
|
8
|
-
export { deploy, parseBindings, uninstall, upgrade } from "./deploy.js";
|
|
8
|
+
export { deploy, MAX_YANK_REASON, parseBindings, runJob, uninstall, upgrade, yank } from "./deploy.js";
|
|
9
|
+
export { contractConfig, emulatorConfig, secretEnv } from "./local-run.js";
|
|
9
10
|
export { CONFIG_FILE, defaultContract, hostedRuleProblems, loadProject, MANIFEST_FILE, migrationProblems, readMigrations, readUiFiles, uiFrames, } from "./project.js";
|
|
10
11
|
// The bundle lint is the published contract's, the same function core's installer runs.
|
|
11
12
|
export { lintBundle, MAX_BUNDLE_BYTES } from "@sparelabs/sightline-extension-manifest";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ContractConfig, type PermissionCheckConfig, type PersonConfig, type Project, type UserConfig, type WorkspaceConfig } from "./project.ts";
|
|
2
|
+
/** The devkit's `EmulatorOptions`, as far as the CLI fills them. */
|
|
3
|
+
export interface EmulatorConfig {
|
|
4
|
+
extension: string;
|
|
5
|
+
version: string;
|
|
6
|
+
settings: Record<string, unknown>;
|
|
7
|
+
manifest: Record<string, unknown>;
|
|
8
|
+
/** SL_SECRET_<NAME> → value: the runtime gets them, the ext-hooks route verifies with them. */
|
|
9
|
+
secrets: Record<string, string>;
|
|
10
|
+
users?: UserConfig[];
|
|
11
|
+
workspace?: WorkspaceConfig;
|
|
12
|
+
people?: PersonConfig[];
|
|
13
|
+
coreScopes?: string[];
|
|
14
|
+
}
|
|
15
|
+
/** The testing package's `ContractOptions`, minus the URLs. */
|
|
16
|
+
export interface ContractRunConfig {
|
|
17
|
+
tool: NonNullable<ContractConfig["tool"]>;
|
|
18
|
+
permissions?: PermissionCheckConfig[];
|
|
19
|
+
job?: ContractConfig["job"];
|
|
20
|
+
hook?: ContractConfig["hook"];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The extension's secrets as the runtime gets them: every SL_SECRET_* in the
|
|
24
|
+
* environment, then `devSecrets` on top (their names upper-cased, `-` → `_`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function secretEnv(p: Project, env: Record<string, string | undefined>): Record<string, string>;
|
|
27
|
+
/** The emulator `dev` and `test` start, from the project and its sightline-ext.json. */
|
|
28
|
+
export declare function emulatorConfig(p: Project, secrets: Record<string, string>): EmulatorConfig;
|
|
29
|
+
/** What `test` asks the contract kit to check: the tool (the manifest's first by default), permissions, job, hook. */
|
|
30
|
+
export declare function contractConfig(p: Project): ContractRunConfig;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// What `dev` and `test` hand the devkit's emulator and the contract kit, from
|
|
2
|
+
// the project and its sightline-ext.json: the manifest (its webhooks, its
|
|
3
|
+
// required core scopes), devSecrets, users, workspace, people, coreScopes, and
|
|
4
|
+
// the contract's tool, permissions, job and hook. Pure, so each field is tested
|
|
5
|
+
// without starting anything (test/cli.test.ts).
|
|
6
|
+
import { CONFIG_FILE, defaultContract } from "./project.js";
|
|
7
|
+
const SECRET_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
|
|
8
|
+
const where = (field) => `${CONFIG_FILE}: ${field}`;
|
|
9
|
+
const isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
10
|
+
const isStrings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
11
|
+
/**
|
|
12
|
+
* The extension's secrets as the runtime gets them: every SL_SECRET_* in the
|
|
13
|
+
* environment, then `devSecrets` on top (their names upper-cased, `-` → `_`).
|
|
14
|
+
*/
|
|
15
|
+
export function secretEnv(p, env) {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [k, v] of Object.entries(env))
|
|
18
|
+
if (k.startsWith("SL_SECRET_") && v !== undefined)
|
|
19
|
+
out[k] = v;
|
|
20
|
+
const dev = p.config.devSecrets ?? {};
|
|
21
|
+
if (!isObject(dev))
|
|
22
|
+
throw new Error(where("devSecrets must be an object of secret name → value"));
|
|
23
|
+
for (const [name, value] of Object.entries(dev)) {
|
|
24
|
+
if (!SECRET_NAME_RE.test(name) || typeof value !== "string")
|
|
25
|
+
throw new Error(`devSecrets: "${name}" must be a secret name with a string value`);
|
|
26
|
+
out[`SL_SECRET_${name.toUpperCase().replace(/-/g, "_")}`] = value;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function checkUsers(users) {
|
|
31
|
+
if (!Array.isArray(users))
|
|
32
|
+
throw new Error(where('users must be [{ "name": "alice", "email"?: "…", "grants"?: ["…"] }]'));
|
|
33
|
+
return users.map((u, i) => {
|
|
34
|
+
if (!isObject(u) || typeof u.name !== "string" || u.name === "")
|
|
35
|
+
throw new Error(where(`users[${i}] needs a "name"`));
|
|
36
|
+
if (u.email !== undefined && typeof u.email !== "string")
|
|
37
|
+
throw new Error(where(`users[${i}].email must be a string`));
|
|
38
|
+
if (u.grants !== undefined && !isStrings(u.grants))
|
|
39
|
+
throw new Error(where(`users[${i}].grants must be a list of permission keys`));
|
|
40
|
+
if (u.access !== undefined && !isObject(u.access))
|
|
41
|
+
throw new Error(where(`users[${i}].access must be { "<ext>.<resource>": [read, write] }`));
|
|
42
|
+
return u;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function checkWorkspace(workspace) {
|
|
46
|
+
if (!isObject(workspace))
|
|
47
|
+
throw new Error(where('workspace must be { "name"?, "timezone"?, "locale"?, "weekStart"? }'));
|
|
48
|
+
for (const k of ["name", "timezone", "locale"]) {
|
|
49
|
+
if (workspace[k] !== undefined && typeof workspace[k] !== "string")
|
|
50
|
+
throw new Error(where(`workspace.${k} must be a string`));
|
|
51
|
+
}
|
|
52
|
+
const ws = workspace.weekStart;
|
|
53
|
+
if (ws !== undefined && (typeof ws !== "number" || !Number.isInteger(ws) || ws < 1 || ws > 7))
|
|
54
|
+
throw new Error(where("workspace.weekStart is 1 (Monday) to 7 (Sunday)"));
|
|
55
|
+
return workspace;
|
|
56
|
+
}
|
|
57
|
+
function checkPeople(people) {
|
|
58
|
+
if (!Array.isArray(people))
|
|
59
|
+
throw new Error(where('people must be [{ "displayName": "…", "id"?, "title"?, "department"?, "avatarUrl"?, "active"? }]'));
|
|
60
|
+
people.forEach((p, i) => {
|
|
61
|
+
if (!isObject(p) || typeof p.displayName !== "string")
|
|
62
|
+
throw new Error(where(`people[${i}] needs a "displayName"`));
|
|
63
|
+
});
|
|
64
|
+
return people;
|
|
65
|
+
}
|
|
66
|
+
function checkPermissions(permissions) {
|
|
67
|
+
if (!Array.isArray(permissions))
|
|
68
|
+
throw new Error(where('contract.permissions must be [{ "tool": "<name>" | "api": "<METHOD> /path", "grants": ["…"] }]'));
|
|
69
|
+
return permissions.map((c, i) => {
|
|
70
|
+
const at = `contract.permissions[${i}]`;
|
|
71
|
+
if (!isObject(c))
|
|
72
|
+
throw new Error(where(`${at} must be an object`));
|
|
73
|
+
if ((typeof c.tool === "string") === (typeof c.api === "string"))
|
|
74
|
+
throw new Error(where(`${at} names exactly one of "tool" and "api"`));
|
|
75
|
+
if (typeof c.api === "string" && !/^[A-Z]+ \/\S*$/.test(c.api))
|
|
76
|
+
throw new Error(where(`${at}.api is "<METHOD> /path", e.g. "POST /kudos"`));
|
|
77
|
+
if (!isStrings(c.grants))
|
|
78
|
+
throw new Error(where(`${at}.grants must be a list of permission keys`));
|
|
79
|
+
if (c.as !== undefined && typeof c.as !== "string")
|
|
80
|
+
throw new Error(where(`${at}.as must be "user:<name>"`));
|
|
81
|
+
return c;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** The emulator `dev` and `test` start, from the project and its sightline-ext.json. */
|
|
85
|
+
export function emulatorConfig(p, secrets) {
|
|
86
|
+
const c = p.config;
|
|
87
|
+
const out = { extension: p.id, version: p.version, settings: c.settings ?? {}, manifest: p.manifest, secrets };
|
|
88
|
+
if (c.users !== undefined)
|
|
89
|
+
out.users = checkUsers(c.users);
|
|
90
|
+
if (c.workspace !== undefined)
|
|
91
|
+
out.workspace = checkWorkspace(c.workspace);
|
|
92
|
+
if (c.people !== undefined)
|
|
93
|
+
out.people = checkPeople(c.people);
|
|
94
|
+
if (c.coreScopes !== undefined) {
|
|
95
|
+
if (!isStrings(c.coreScopes))
|
|
96
|
+
throw new Error(where('coreScopes must be a list of core scopes, e.g. ["core:people:read"]'));
|
|
97
|
+
out.coreScopes = c.coreScopes;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** What `test` asks the contract kit to check: the tool (the manifest's first by default), permissions, job, hook. */
|
|
102
|
+
export function contractConfig(p) {
|
|
103
|
+
const contract = defaultContract(p);
|
|
104
|
+
const tool = contract.tool;
|
|
105
|
+
if (tool.grants !== undefined && !isStrings(tool.grants))
|
|
106
|
+
throw new Error(where("contract.tool.grants must be a list of permission keys"));
|
|
107
|
+
const out = { tool };
|
|
108
|
+
if (contract.permissions !== undefined)
|
|
109
|
+
out.permissions = checkPermissions(contract.permissions);
|
|
110
|
+
if (contract.job)
|
|
111
|
+
out.job = contract.job;
|
|
112
|
+
if (contract.hook)
|
|
113
|
+
out.hook = contract.hook;
|
|
114
|
+
return out;
|
|
115
|
+
}
|
package/dist/project.d.ts
CHANGED
|
@@ -19,11 +19,23 @@ export declare const MAX_MIGRATION_BYTES: number;
|
|
|
19
19
|
export declare const MAX_MIGRATIONS = 200;
|
|
20
20
|
export declare const EXT_ID_RE: RegExp;
|
|
21
21
|
export declare const MIGRATION_FILE_RE: RegExp;
|
|
22
|
+
/** A permission-checked tool or /api route `test` calls with and without its grants (the testing package's `PermissionCheck`). */
|
|
23
|
+
export interface PermissionCheckConfig {
|
|
24
|
+
/** A tool name. Name exactly one of `tool` and `api`. */
|
|
25
|
+
tool?: string;
|
|
26
|
+
/** An /api route as `"<METHOD> /path"`, e.g. `"POST /kudos"`. */
|
|
27
|
+
api?: string;
|
|
28
|
+
input?: unknown;
|
|
29
|
+
as?: string;
|
|
30
|
+
grants: string[];
|
|
31
|
+
}
|
|
22
32
|
export interface ContractConfig {
|
|
33
|
+
/** `grants`: what `as` holds for these calls; absent, what `users` gives them. */
|
|
23
34
|
tool?: {
|
|
24
35
|
name: string;
|
|
25
36
|
input?: Record<string, unknown>;
|
|
26
37
|
as?: string;
|
|
38
|
+
grants?: string[];
|
|
27
39
|
};
|
|
28
40
|
job?: {
|
|
29
41
|
name: string;
|
|
@@ -33,17 +45,53 @@ export interface ContractConfig {
|
|
|
33
45
|
name: string;
|
|
34
46
|
body?: unknown;
|
|
35
47
|
};
|
|
48
|
+
/** Each must succeed with its grants and be refused (permission_denied / scope_missing / 403) with none. */
|
|
49
|
+
permissions?: PermissionCheckConfig[];
|
|
50
|
+
}
|
|
51
|
+
/** An emulated person who can call the extension (`--as user:<name>`): the devkit's `DevUser`. */
|
|
52
|
+
export interface UserConfig {
|
|
53
|
+
name: string;
|
|
54
|
+
email?: string;
|
|
55
|
+
/** The permission keys they hold: their tokens' scopes. */
|
|
56
|
+
grants?: string[];
|
|
57
|
+
/** Their bits on the extension's own resources (`{ "<ext>.<resource>": [read, write] }`): the token's `access`. */
|
|
58
|
+
access?: Record<string, [number, number]>;
|
|
59
|
+
}
|
|
60
|
+
/** A person in the emulated people directory (/v1/people): the devkit's `DevPerson`. */
|
|
61
|
+
export interface PersonConfig {
|
|
62
|
+
id?: string;
|
|
63
|
+
displayName: string;
|
|
64
|
+
avatarUrl?: string | null;
|
|
65
|
+
title?: string | null;
|
|
66
|
+
department?: string | null;
|
|
67
|
+
active?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** The workspace /v1/me reports (default: "Dev workspace", UTC, en-US, Sunday). */
|
|
70
|
+
export interface WorkspaceConfig {
|
|
71
|
+
name?: string;
|
|
72
|
+
timezone?: string;
|
|
73
|
+
locale?: string;
|
|
74
|
+
/** ISO: 1 = Monday … 7 = Sunday. */
|
|
75
|
+
weekStart?: 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
36
76
|
}
|
|
37
77
|
/** sightline-ext.json: how `dev` and `test` run the extension locally. Never deployed. */
|
|
38
78
|
export interface ProjectConfig {
|
|
39
79
|
/** The server entry (default server/main.ts). */
|
|
40
80
|
entry?: string;
|
|
41
|
-
/** What `test` checks: a tool that succeeds, and optionally a job and a hook. */
|
|
81
|
+
/** What `test` checks: a tool that succeeds, permission checks, and optionally a job and a hook. */
|
|
42
82
|
contract?: ContractConfig;
|
|
43
83
|
/** The installation settings the emulated core answers with. */
|
|
44
84
|
settings?: Record<string, unknown>;
|
|
45
|
-
/** Development-only secret values, injected as SL_SECRET_<NAME
|
|
85
|
+
/** Development-only secret values, injected as SL_SECRET_<NAME> (and verified with by the emulator's ext-hooks). Never deployed. */
|
|
46
86
|
devSecrets?: Record<string, string>;
|
|
87
|
+
/** The emulated people and their grants (default: alice and bob, holding nothing). */
|
|
88
|
+
users?: UserConfig[];
|
|
89
|
+
/** The workspace the emulated core reports. */
|
|
90
|
+
workspace?: WorkspaceConfig;
|
|
91
|
+
/** The emulated people directory (default: one person per user). */
|
|
92
|
+
people?: PersonConfig[];
|
|
93
|
+
/** The core scopes the installation holds (default: the manifest's `scopes.required`, as installing grants them). */
|
|
94
|
+
coreScopes?: string[];
|
|
47
95
|
/** The built UI `deploy` uploads with the release, for ui.frames (default dist/ui; build it with your own tool first). */
|
|
48
96
|
ui?: {
|
|
49
97
|
dir?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparelabs/sightline-extension-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "sightline-ext: scaffold, run, build, test and deploy a hosted Sightline extension.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Spare Labs Inc.",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@sparelabs/sightline-extension-devkit": "0.1.
|
|
37
|
+
"@sparelabs/sightline-extension-devkit": "0.1.4",
|
|
38
38
|
"@sparelabs/sightline-extension-manifest": "0.1.2",
|
|
39
39
|
"@sparelabs/sightline-extension-testing": "0.1.0",
|
|
40
40
|
"esbuild": "^0.25.10"
|