@pitcher/cli-scripts 2.18.0 → 2.20.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 +45 -10
- package/dist/src/commands/analytics.d.ts +3 -1
- package/dist/src/commands/analytics.js +7 -3
- package/dist/src/commands/devSync.d.ts +2 -0
- package/dist/src/commands/devSync.js +5 -5
- package/dist/src/commands/dsr.d.ts +5 -0
- package/dist/src/commands/dsr.js +126 -14
- package/dist/src/commands/publishApp.d.ts +2 -0
- package/dist/src/commands/publishApp.js +3 -3
- package/dist/src/commands/scaffolds.js +11 -3
- package/dist/src/core/archive.js +3 -3
- package/dist/src/core/credential.d.ts +25 -0
- package/dist/src/core/credential.js +25 -0
- package/dist/src/core/http.d.ts +6 -1
- package/dist/src/core/http.js +6 -5
- package/dist/src/core/ld.d.ts +38 -2
- package/dist/src/core/ld.js +162 -6
- package/dist/src/core/readInput.js +8 -1
- package/dist/src/mcp/bundled.js +1 -1
- package/dist/src/mcp/hosted.d.ts +21 -0
- package/dist/src/mcp/hosted.js +82 -0
- package/dist/src/mcp/index.d.ts +7 -0
- package/dist/src/mcp/index.js +6 -0
- package/dist/src/mcp/serve.js +1 -1
- package/dist/src/mcp/server.d.ts +19 -2
- package/dist/src/mcp/server.js +4 -4
- package/dist/src/mcp/tools/analytics.js +1 -1
- package/dist/src/mcp/tools/devSync.js +2 -2
- package/dist/src/mcp/tools/dsr.js +19 -1
- package/dist/src/mcp/tools/publishApp.js +2 -1
- package/dist/src/mcp/tools/types.d.ts +1 -1
- package/package.json +17 -6
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ p command --help
|
|
|
12
12
|
|
|
13
13
|
## MCP server
|
|
14
14
|
|
|
15
|
-
Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps
|
|
15
|
+
Run `p mcp` to start a stdio [MCP](https://modelcontextprotocol.io) server that exposes this CLI's commands as tools to an MCP-capable host (Claude Desktop, Claude Code, etc). It wraps one tool per CLI `run*` function: every command below (including `dsr create`/`get`/`update`/`delete`/`bind-landing` and the full `ld` surface) except the five local `scaffold` subcommands (which just print/write a starter file and make no request). `dev-sync` is exposed as a one-shot sync only; the continuous `watch` mode is not available as a tool.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
p mcp
|
|
@@ -26,7 +26,7 @@ Auth and org scope are resolved once at boot from environment variables. They ar
|
|
|
26
26
|
|
|
27
27
|
- `NG_API_KEY` (required, secret): the API key the server authenticates as.
|
|
28
28
|
- `NG_ORG` (default `dev`): organization subdomain, or set `NG_URL` directly for a full origin.
|
|
29
|
-
- `NG_ENV` (`prod` or `test`): controls the destructive-hint guard on write tools (see below).
|
|
29
|
+
- `NG_ENV` (`prod` or `test`): controls the destructive-hint guard on write tools (see below). When unset, anything other than the `dev` org (or, if `NG_URL` is set, anything other than `https://dev.my.pitcher.com`) is treated as production.
|
|
30
30
|
|
|
31
31
|
One config entry = one org, because the API key is org-specific. To work against multiple orgs, add multiple entries (e.g. `pitcher-dev`, `pitcher-prod`) with different env blocks; do not try to pass org per tool call.
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@ One config entry = one org, because the API key is org-specific. To work against
|
|
|
43
43
|
### Tool annotations
|
|
44
44
|
|
|
45
45
|
- Read commands are marked `readOnlyHint`. `dev-sync` counts as a write (it mutates remote dev-override state).
|
|
46
|
-
- `publish-app` is always marked `destructiveHint` (it creates a live, org-visible release). Other write tools get `destructiveHint` only when the resolved environment is production (`NG_ENV=prod`, or `
|
|
46
|
+
- `publish-app` is always marked `destructiveHint` (it creates a live, org-visible release). Other write tools get `destructiveHint` only when the resolved environment is production (`NG_ENV=prod`, or — with `NG_ENV` unset — a target other than `dev` / `dev.my.pitcher.com`). This steers hosts toward requiring extra confirmation before mutating a production org.
|
|
47
47
|
|
|
48
48
|
### Desktop Extension (`.mcpb`)
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ yarn mcpb:pack # → pitcher.mcpb (validated + packed)
|
|
|
55
55
|
|
|
56
56
|
Install by double-clicking `pitcher.mcpb` (or Settings → Extensions → Install Extension). Claude Desktop prompts for the API key (stored in the OS keychain) and organization; both are editable afterward, so switching orgs needs no reinstall.
|
|
57
57
|
|
|
58
|
-
The bundle reuses the same server as `p mcp` but registers a **content-tool subset (
|
|
58
|
+
The bundle reuses the same server as `p mcp` but registers a **content-tool subset (121)** — it omits the developer-only `publish-app` and `dev-sync`/`dev-sync-revert`. The `organization` field accepts a subdomain (`acme`), a bare hostname (`acme.my.pitcher.com`), or a full `https://` URL. Build details (esbuild single-file, version injection) live in `mcpb/` and `CLAUDE.md`.
|
|
59
59
|
|
|
60
60
|
This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the `pitcher-apps` skill, not this README.
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ This section is a reference only. For app-development conventions (modules, sett
|
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
# Basic usage
|
|
66
|
-
p publish-app <folder> --
|
|
66
|
+
p publish-app <folder> --org pitcher --api-key myapikey
|
|
67
67
|
|
|
68
68
|
# Publish to current folder and default org (dev)
|
|
69
69
|
p publish-app --api-key myapikey
|
|
@@ -506,7 +506,7 @@ p metadata-template upsert --instance-id 01... --sections-file ./schema.json --a
|
|
|
506
506
|
|
|
507
507
|
## To run the `dsr` command:
|
|
508
508
|
|
|
509
|
-
Create and share a Digital Sales Room (a canvas share link) and get a link back in one command.
|
|
509
|
+
Create and share a Digital Sales Room (a canvas share link) and get a link back in one command. Six subcommands: `create` (build + share, with optional landing-page binding), `get` (read an existing share back), `update` (change or clear the access settings of an existing share), `delete` (revoke a share), `bind-landing` (bind/unbind a landing template on an existing share), and `scaffold` (print a starter `{source,security,landing}` spec for `--config-file`).
|
|
510
510
|
|
|
511
511
|
The create endpoint is a **get-or-create**: sharing an already-shared canvas returns the existing link and updates only the fields you pass. That is deliberate (it mirrors Admin's "Create link once / Copy link after"), but it means `create` with individual flags can never *remove* a value — omitting a flag leaves the stored value alone. Use `update` for that, and `--clear <field>` to remove one.
|
|
512
512
|
|
|
@@ -519,11 +519,16 @@ p dsr create --instance-id 01... --canvas-id 01... --api-key myapikey
|
|
|
519
519
|
p dsr create --instance-id 01... --canvas-id 01... --auth-strategy none
|
|
520
520
|
# ShareBox + security + branded landing
|
|
521
521
|
p dsr create --instance-id 01... --sharebox-file-ids 01A,01B --account-id 01... \
|
|
522
|
-
--auth-strategy specified_domains --allowed-domains acme.com --valid-until 2026-
|
|
522
|
+
--auth-strategy specified_domains --allowed-domains acme.com --valid-until 2026-12-01T00:00:00Z \
|
|
523
523
|
--landing-template-id 01... --landing-context '{"rep":"Sam"}'
|
|
524
524
|
# bind/unbind a landing template on an existing share
|
|
525
525
|
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01...
|
|
526
526
|
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id none
|
|
527
|
+
# merge fields from a FILE instead of inline JSON (the only spelling that works in PowerShell)
|
|
528
|
+
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
|
|
529
|
+
p dsr create --instance-id 01... --canvas-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
|
|
530
|
+
# read an existing share back
|
|
531
|
+
p dsr get --id 01...
|
|
527
532
|
# refuse to reuse an existing share instead of silently returning it
|
|
528
533
|
p dsr create --canvas-id 01... --if-exists error
|
|
529
534
|
# send the download preference explicitly — either direction
|
|
@@ -545,7 +550,11 @@ p dsr scaffold --out share.json
|
|
|
545
550
|
|
|
546
551
|
`p dsr scaffold` prints the starter. The spec has three blocks — `source`, `security`, `landing` — and the CLI sends `source` and `security` **spread verbatim into the create POST body**, so their keys are the wire (snake_case) names, not flag names. `landing` is not part of that body: it becomes a follow-up PUT to `/{id}/landing-page` after the share exists.
|
|
547
552
|
|
|
548
|
-
The `source` block picks what is shared:
|
|
553
|
+
The `source` block picks what is shared. The scaffold emits `canvas_id` (an example value) **and** an empty `sharebox_items: []`, so both source keys are visible in the printed JSON; an empty array asks for nothing, so the untouched scaffold shares `canvas_id`. To share files instead, fill `sharebox_items`, delete the `canvas_id` line, and add `instance_id` (or pass `--instance-id`). `p dsr create --help` lists the same field names.
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
"source": { "instance_id": "01...", "sharebox_items": [{ "file_id": "01..." }, { "file_id": "01...", "page_index": 2 }] }
|
|
557
|
+
```
|
|
549
558
|
|
|
550
559
|
| Key | Type | Meaning |
|
|
551
560
|
|-----|------|---------|
|
|
@@ -558,7 +567,7 @@ The `source` block picks what is shared:
|
|
|
558
567
|
|
|
559
568
|
`canvas_name`, `account_id`, `template_id` and `security.tags` name or shape the canvas the server **mints**, so they only mean anything for a `sharebox_items` source — with `canvas_id` the canvas already exists and there is nothing to name. `tags` is the one the CLI enforces: a non-empty `tags` alongside `canvas_id` is rejected rather than sent to be dropped. The scaffold's empty `tags: []` is accepted, since it asks for nothing.
|
|
560
569
|
|
|
561
|
-
`security` holds the access settings (`auth_strategy`, `allowed_emails`, `allowed_domains`, `valid_until`, `allow_forward`, `allow_download`, `tags`) and every key in it is spread into the POST body **exactly as written**. The CLI checks three of them and no more: `auth_strategy` against the allowed values, the `allowed_emails`/`allowed_domains` that strategy requires, and `tags` (above). The rest — `allow_download` included — it forwards untouched and makes no claim about, so read the created share back if you need to confirm a value took. Sending the block as written also matters against an already-shared canvas — see the `--if-exists` note above. `landing` holds `landing_page_template_id` (discover ids with `p dsr-landing list`) and `landing_page_context`; left at the scaffold's `null` / `{}` no landing page is bound at all. Binding one — from the spec or from `--landing-template-id` — needs an instance: `--instance-id` or `source.instance_id`. Without it the CLI skips the bind and returns the share with a warning naming the flag.
|
|
570
|
+
`security` holds the access settings (`auth_strategy`, `allowed_emails`, `allowed_domains`, `valid_until`, `allow_forward`, `allow_download`, `tags`) and every key in it is spread into the POST body **exactly as written**. The CLI checks three of them and no more: `auth_strategy` against the allowed values, the `allowed_emails`/`allowed_domains` that strategy requires, and `tags` (above). The rest — `allow_download` included — it forwards untouched and makes no claim about, so read the created share back if you need to confirm a value took. The scaffold deliberately **omits** `allow_download`: it is stored on the canvas (every link on that canvas reads it), and the server writes it only when the key is present — omitted, an existing canvas keeps its current value and a new ShareBox canvas carries none, which the viewer treats as **downloads allowed**. Add `"allow_download": false` to the spec to turn downloads off. Sending the block as written also matters against an already-shared canvas — see the `--if-exists` note above. `landing` holds `landing_page_template_id` (discover ids with `p dsr-landing list`) and `landing_page_context`; left at the scaffold's `null` / `{}` no landing page is bound at all. Binding one — from the spec or from `--landing-template-id` — needs an instance: `--instance-id` or `source.instance_id`. Without it the CLI skips the bind and returns the share with a warning naming the flag.
|
|
562
571
|
|
|
563
572
|
`update` PATCHes `/core/api/protected/shared-links/{id}`, which distinguishes an **absent** key from an **explicit null** — so omitting `--valid-until` leaves the stored expiry exactly as it is, and `--clear valid-until` is what resets it. `--clear` is repeatable and accepts `valid-until`, `allowed-emails`, `allowed-domains` (the CLI sends `null` for the first and `[]` for the lists, matching what each field's server schema accepts). An empty `--allowed-emails`/`--allowed-domains` is rejected on both `create` and `update`, because `""` from an unset shell variable would otherwise be sent as `[]` and wipe the stored list. `--clear` is a separate flag rather than an in-value sentinel such as `--valid-until ""` because an empty string is something a script can produce by accident from an unset variable, and `none` is already a real `--auth-strategy` value. `tags` and `--allow-download` are **create-time only** — the PATCH surface does not accept them, and `--clear` says so rather than dropping them silently.
|
|
564
573
|
|
|
@@ -573,9 +582,35 @@ p dsr update --id 01... --auth-strategy any_email --clear allowed-emails
|
|
|
573
582
|
p dsr update --id 01... --allowed-emails new@acme.com
|
|
574
583
|
```
|
|
575
584
|
|
|
585
|
+
### `--landing-context` from a file
|
|
586
|
+
|
|
587
|
+
`--landing-context` takes a JSON object as a single shell argument, which **PowerShell cannot express**: it strips the double quotes a JSON object cannot do without, so on Windows the flag has no usable spelling at all. `--landing-context-file <path>` is the same field read off disk, and it is available on **both** `dsr create` and `dsr bind-landing`:
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
echo '{"rep":"Sam","account":"Acme"}' > ctx.json
|
|
591
|
+
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
Exactly one of the two may be given: passing **both** fails `VALIDATION` (exit 3) before any request, naming the pair. The file must hold a JSON **object** — valid JSON that is an array or a scalar is rejected with the same code, as is malformed JSON. `--landing-context-file` is a landing flag like the rest, so it is mutually exclusive with `--config-file` (whose `landing.landing_page_context` block is that spec's own way of saying the same thing).
|
|
595
|
+
|
|
596
|
+
### `p dsr get` — read a share back
|
|
597
|
+
|
|
598
|
+
`p dsr get --id <id>` returns the share's current settings. The protected shared-links surface has **no GET**, so this wraps the public pre-auth config the sharing page itself loads (`GET /core/api/shared-links/{id}/config`) and returns that body **unchanged**. Nothing is reshaped, so the field names are the wire names.
|
|
599
|
+
|
|
600
|
+
That body is the share's **access config**, and nothing else: `auth_strategy`, `allowed_domains`, `allowed_emails`, `valid_until`, `allow_forward` (plus `allow_forward_raw`, the rep's own toggle before the admin `allow_dsr_forwarding` setting is applied), the landing-page binding as `landing_page_template_id` / `landing_page_template_snapshot` / `landing_page_context`, and `created_at` / `modified_at`. **There is no `canvas` or `file` field** — the endpoint selects neither, so hold on to the `canvas` that `create` returned if you need it.
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
p dsr get --id 01...
|
|
604
|
+
# confirm what a create actually stored
|
|
605
|
+
p dsr create --instance-id 01... --canvas-id 01... --auth-strategy specified_domains --allowed-domains acme.com
|
|
606
|
+
p dsr get --id 01...
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Use it to confirm what `create` or `update` stored (`create` is a get-or-create, so what you passed is not always what the share ends up with) and to check a link before changing it. There is **one** id failure mode: any id that matches no share answers `404` → `NOT_FOUND` (exit 5). The endpoint does not validate the id's format — it matches `pitcher_sharedlink.id`, a plain varchar — so a malformed id is a miss like any other and returns `NOT_FOUND` too, not `VALIDATION`.
|
|
610
|
+
|
|
576
611
|
`create` returns the share as `{ id, type, file, canvas, instance, organization, status, url, short_url }` — **`url` is the full link; there is no `full_url` field.**
|
|
577
612
|
|
|
578
|
-
Creating, updating and deleting need a share-capable key (`canvas.share` on the link's own instance when `enable_granular_permissions` is on); **binding a landing page** needs `--instance-id`, and an admin or editor key to bind any share in the org — a member key can bind only the shares that member created (PIT-7525). On `create`, a denied bind returns the link with a warning. Discover landing template IDs via `p dsr-landing list`.
|
|
613
|
+
`get` reads a public, pre-auth endpoint, so it needs no special permission beyond a valid key. Creating, updating and deleting need a share-capable key (`canvas.share` on the link's own instance when `enable_granular_permissions` is on); **binding a landing page** needs `--instance-id`, and an admin or editor key to bind any share in the org — a member key can bind only the shares that member created (PIT-7525). On `create`, a denied bind returns the link with a warning. Sending `--allow-download` / `--no-allow-download` (or `allow_download` in a spec) with an existing `--canvas-id` needs you to own that canvas or hold `canvas.update` on it — the setting is stored on the canvas — otherwise the create is refused as `AUTH` and nothing is written; a ShareBox source is always yours, so it never hits this. Discover landing template IDs via `p dsr-landing list`.
|
|
579
614
|
|
|
580
615
|
## To run the `ld` command:
|
|
581
616
|
|
|
@@ -22,7 +22,9 @@ export interface AnalyticsKpiOpts extends CommonOpts {
|
|
|
22
22
|
* org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
|
|
23
23
|
* org api-key; the caller inherits the key user's real role. Dates are required
|
|
24
24
|
* by the server for activity-metric types (inventory counts like
|
|
25
|
-
* `total-members` need none) — that validation stays server-side
|
|
25
|
+
* `total-members` need none) — that validation stays server-side, as does the
|
|
26
|
+
* set of valid `type` values (a bad one comes back as a 400 listing them). There
|
|
27
|
+
* is no aggregate `all` type, so `--type` is required.
|
|
26
28
|
*/
|
|
27
29
|
export declare function runAnalyticsKpi(opts: AnalyticsKpiOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
28
30
|
export interface AnalyticsEngagementOpts extends CommonOpts {
|
|
@@ -32,16 +32,20 @@ export async function runAnalyticsInstance(opts, deps = {}) {
|
|
|
32
32
|
* org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
|
|
33
33
|
* org api-key; the caller inherits the key user's real role. Dates are required
|
|
34
34
|
* by the server for activity-metric types (inventory counts like
|
|
35
|
-
* `total-members` need none) — that validation stays server-side
|
|
35
|
+
* `total-members` need none) — that validation stays server-side, as does the
|
|
36
|
+
* set of valid `type` values (a bad one comes back as a 400 listing them). There
|
|
37
|
+
* is no aggregate `all` type, so `--type` is required.
|
|
36
38
|
*/
|
|
37
39
|
export async function runAnalyticsKpi(opts, deps = {}) {
|
|
38
40
|
try {
|
|
39
41
|
if (!opts.instanceId)
|
|
40
42
|
throw createCliError('VALIDATION', '--instance-id is required for analytics kpi');
|
|
43
|
+
if (!opts.type)
|
|
44
|
+
throw createCliError('VALIDATION', '--type is required for analytics kpi');
|
|
41
45
|
const client = clickhouseClientFor(opts, deps);
|
|
42
46
|
const params = {
|
|
43
47
|
instance_id: opts.instanceId,
|
|
44
|
-
type: opts.type
|
|
48
|
+
type: opts.type,
|
|
45
49
|
};
|
|
46
50
|
if (opts.startDate)
|
|
47
51
|
params.start_date = opts.startDate;
|
|
@@ -376,7 +380,7 @@ export function createAnalyticsCommand() {
|
|
|
376
380
|
});
|
|
377
381
|
withAuthFlags(cmd.command('kpi'))
|
|
378
382
|
.option('--instance-id <id>', 'Instance ID (required)')
|
|
379
|
-
.option('--type <type>',
|
|
383
|
+
.option('--type <type>', 'KPI type, e.g. total-members, total-active-users (required)')
|
|
380
384
|
.option('--start-date <iso>', 'Window start (ISO date; required by the server for activity metrics)')
|
|
381
385
|
.option('--end-date <iso>', 'Window end (ISO date; required by the server for activity metrics)')
|
|
382
386
|
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs to scope by')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { AxiosInstance } from 'axios';
|
|
3
|
+
import { type Credential } from '../core/credential.js';
|
|
3
4
|
import { type Envelope } from '../core/envelope.js';
|
|
4
5
|
interface AppJson {
|
|
5
6
|
name: string;
|
|
@@ -17,6 +18,7 @@ interface SyncResponse {
|
|
|
17
18
|
interface DevSyncOptions {
|
|
18
19
|
fileId: string;
|
|
19
20
|
apiKey?: string;
|
|
21
|
+
credential?: Credential;
|
|
20
22
|
org?: string;
|
|
21
23
|
quiet?: boolean;
|
|
22
24
|
output?: string;
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import FormData from 'form-data';
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
|
-
import {
|
|
6
|
+
import { credentialOf } from '../core/credential.js';
|
|
7
7
|
import { resolveBaseUrl } from '../core/baseUrl.js';
|
|
8
8
|
import { makeClient, toCliError } from '../core/http.js';
|
|
9
9
|
import { zipDirectoryToBuffer } from '../core/archive.js';
|
|
@@ -32,11 +32,11 @@ export function readAppJson(distPath) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function runDevSync(opts, deps = {}) {
|
|
34
34
|
try {
|
|
35
|
-
const
|
|
35
|
+
const cred = credentialOf(opts);
|
|
36
36
|
const baseUrl = resolveBaseUrl(opts);
|
|
37
37
|
const distPath = findDistPath(deps.cwd);
|
|
38
38
|
const appJson = readAppJson(distPath);
|
|
39
|
-
const client = deps.client ?? makeClient(baseUrl,
|
|
39
|
+
const client = deps.client ?? makeClient(baseUrl, cred);
|
|
40
40
|
log(`Syncing ${appJson.name} from ${distPath}...`, { quiet: opts.quiet });
|
|
41
41
|
const zip = await zipDirectoryToBuffer(distPath);
|
|
42
42
|
log(`Zip created: ${(zip.length / 1024).toFixed(2)} KB`, { quiet: opts.quiet });
|
|
@@ -57,9 +57,9 @@ export async function runDevSync(opts, deps = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
export async function runRevert(opts, deps = {}) {
|
|
59
59
|
try {
|
|
60
|
-
const
|
|
60
|
+
const cred = credentialOf(opts);
|
|
61
61
|
const baseUrl = resolveBaseUrl(opts);
|
|
62
|
-
const client = deps.client ?? makeClient(baseUrl,
|
|
62
|
+
const client = deps.client ?? makeClient(baseUrl, cred);
|
|
63
63
|
const res = await client.delete(SYNC_ENDPOINT, {
|
|
64
64
|
headers: { 'Content-Type': 'application/json' },
|
|
65
65
|
data: { file_id: opts.fileId },
|
|
@@ -12,6 +12,7 @@ export declare function runDsrBindLanding(opts: CommonOpts & {
|
|
|
12
12
|
id?: string;
|
|
13
13
|
landingTemplateId?: string;
|
|
14
14
|
landingContext?: string;
|
|
15
|
+
landingContextFile?: string;
|
|
15
16
|
instanceId?: string;
|
|
16
17
|
}, deps?: DsrDeps): Promise<Envelope<unknown>>;
|
|
17
18
|
export interface DsrCreateOpts extends CommonOpts {
|
|
@@ -30,6 +31,7 @@ export interface DsrCreateOpts extends CommonOpts {
|
|
|
30
31
|
tags?: string[];
|
|
31
32
|
landingTemplateId?: string;
|
|
32
33
|
landingContext?: string;
|
|
34
|
+
landingContextFile?: string;
|
|
33
35
|
configFile?: string;
|
|
34
36
|
ifExists?: string;
|
|
35
37
|
}
|
|
@@ -48,6 +50,9 @@ export interface DsrUpdateOpts extends CommonOpts {
|
|
|
48
50
|
allowForward?: boolean;
|
|
49
51
|
clear?: string[];
|
|
50
52
|
}
|
|
53
|
+
export declare function runDsrGet(opts: CommonOpts & {
|
|
54
|
+
id?: string;
|
|
55
|
+
}, deps?: DsrDeps): Promise<Envelope<unknown>>;
|
|
51
56
|
export declare function assembleUpdateBody(opts: DsrUpdateOpts): Record<string, unknown>;
|
|
52
57
|
export declare function runDsrUpdate(opts: DsrUpdateOpts, deps?: DsrDeps): Promise<Envelope<unknown>>;
|
|
53
58
|
export declare function runDsrDelete(opts: CommonOpts & {
|
package/dist/src/commands/dsr.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ok, fail } from '../core/envelope.js';
|
|
|
4
4
|
import { createCliError } from '../core/errors.js';
|
|
5
5
|
import { emitEnvelope } from '../core/output.js';
|
|
6
6
|
import { parseJsonObject, parseTags, isPlainObject } from '../schema/content.js';
|
|
7
|
-
import { readTextFile } from '../core/readInput.js';
|
|
7
|
+
import { readTextFile, parseJsonInput } from '../core/readInput.js';
|
|
8
8
|
import { withAuthFlags } from './withAuth.js';
|
|
9
9
|
import { emitScaffold, DSR_SHARE_SCAFFOLD_JSON } from './scaffolds.js';
|
|
10
10
|
const ENDPOINT = '/core/api/protected/shared-links';
|
|
@@ -19,6 +19,46 @@ function resolveTemplateId(raw) {
|
|
|
19
19
|
return null;
|
|
20
20
|
return raw;
|
|
21
21
|
}
|
|
22
|
+
// ── landing context: inline OR file (PIT-7521 QA Q2) ────────────────────────
|
|
23
|
+
// `--landing-context` is a JSON object passed as a shell argument, which PowerShell mangles — it
|
|
24
|
+
// eats the double quotes a JSON object cannot do without, so on Windows the flag has no usable
|
|
25
|
+
// spelling at all. `--landing-context-file` is the way in: same field, read off disk, no quoting.
|
|
26
|
+
//
|
|
27
|
+
// The exclusion check is PRESENCE-based (`!== undefined`), not truthiness, and it runs BEFORE
|
|
28
|
+
// `parseJsonInput`: that helper's own XOR (`resolveTextInput`) is truthy-based, so it would let
|
|
29
|
+
// `--landing-context '' --landing-context-file ctx.json` through and silently read the file. Same
|
|
30
|
+
// reasoning as `metadata-template`'s `--sections` / `--sections-file` pair.
|
|
31
|
+
function resolveLandingContext(inline, file) {
|
|
32
|
+
if (inline !== undefined && file !== undefined)
|
|
33
|
+
throw createCliError('VALIDATION', 'pass exactly one of --landing-context or --landing-context-file');
|
|
34
|
+
if (inline === undefined && file === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
// Exactly one flag is present from here on. An EMPTY string for either flag still short-circuits
|
|
37
|
+
// inside `resolveTextInput`, whose XOR is truthy-based, so `parseJsonInput` would answer `provide
|
|
38
|
+
// --landing-context (inline JSON) or --landing-context-file` -- naming the flag the caller just
|
|
39
|
+
// passed. That is the wrong advice for the accident this command family documents: `--flag "$MAYBE_UNSET"`
|
|
40
|
+
// expanding to an empty string is why `--clear` exists. Name the real problem instead. The
|
|
41
|
+
// genuinely-neither case is returned above and never reaches here, so it keeps that message.
|
|
42
|
+
if (inline === '')
|
|
43
|
+
throw createCliError('VALIDATION', '--landing-context must be valid JSON');
|
|
44
|
+
if (file === '')
|
|
45
|
+
throw createCliError('VALIDATION', '--landing-context-file requires a file path');
|
|
46
|
+
const parsed = parseJsonInput(inline, file, 'landing-context');
|
|
47
|
+
if (!isPlainObject(parsed))
|
|
48
|
+
throw createCliError('VALIDATION', '--landing-context must be a JSON object');
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
// The protected shared-links surface has NO GET — the only readback the platform offers is the
|
|
52
|
+
// PUBLIC pre-auth config the sharing page itself reads. Both callers go through this one function
|
|
53
|
+
// so the URL, the encoding and the error mapping have a single definition: `dsr get`, which returns
|
|
54
|
+
// the body as-is, and `dsr update`'s stored-strategy lookup, which swallows the failure.
|
|
55
|
+
function shareConfigUrl(id) {
|
|
56
|
+
return `/core/api/shared-links/${encodeURIComponent(id)}/config`;
|
|
57
|
+
}
|
|
58
|
+
async function fetchShareConfig(client, id) {
|
|
59
|
+
const res = await client.get(shareConfigUrl(id));
|
|
60
|
+
return res.data;
|
|
61
|
+
}
|
|
22
62
|
// The landing-page PUT is guarded by next-core's `withAtLeastInstanceAdmin`, which resolves the
|
|
23
63
|
// instance from the URL path, then the request body, then the `instance_id` query param. This route
|
|
24
64
|
// has no instance in its path and the body the CLI sends carries only the landing fields, so the
|
|
@@ -45,8 +85,9 @@ export async function runDsrBindLanding(opts, deps = {}) {
|
|
|
45
85
|
if (!opts.instanceId)
|
|
46
86
|
throw createCliError('VALIDATION', LANDING_INSTANCE_REQUIRED);
|
|
47
87
|
const body = { landing_page_template_id: templateId };
|
|
48
|
-
|
|
49
|
-
|
|
88
|
+
const context = resolveLandingContext(opts.landingContext, opts.landingContextFile);
|
|
89
|
+
if (context !== undefined)
|
|
90
|
+
body.landing_page_context = context;
|
|
50
91
|
const client = clientFor(opts, deps);
|
|
51
92
|
const data = await bindLandingRequest(client, opts.id, body, opts.instanceId);
|
|
52
93
|
return ok(data);
|
|
@@ -61,6 +102,7 @@ const AUTH_STRATEGIES = [
|
|
|
61
102
|
const SPEC_FLAG_KEYS = [
|
|
62
103
|
'canvasId', 'shareboxFileIds', 'canvasName', 'accountId', 'templateId', 'authStrategy', 'allowedEmails',
|
|
63
104
|
'allowedDomains', 'validUntil', 'allowForward', 'allowDownload', 'tags', 'landingTemplateId', 'landingContext',
|
|
105
|
+
'landingContextFile',
|
|
64
106
|
];
|
|
65
107
|
// config-file blocks must be JSON objects — a bare cast would silently spread a string/array into the request body
|
|
66
108
|
function requirePlainObjectField(v, field) {
|
|
@@ -118,9 +160,9 @@ function assertCreateBody(body, instanceId) {
|
|
|
118
160
|
const items = body.sharebox_items;
|
|
119
161
|
const hasSharebox = Array.isArray(items) && items.length > 0;
|
|
120
162
|
if (hasCanvas === hasSharebox)
|
|
121
|
-
throw createCliError('VALIDATION', 'pass exactly one source: --canvas-id or --sharebox-file-ids');
|
|
163
|
+
throw createCliError('VALIDATION', 'pass exactly one source: --canvas-id or --sharebox-file-ids (in a --config-file spec: source.canvas_id or a non-empty source.sharebox_items)');
|
|
122
164
|
if (hasSharebox && !instanceId && body.instance_id === undefined)
|
|
123
|
-
throw createCliError('VALIDATION', '--instance-id is required for --sharebox-file-ids');
|
|
165
|
+
throw createCliError('VALIDATION', '--instance-id is required for --sharebox-file-ids (in a --config-file spec: --instance-id or source.instance_id)');
|
|
124
166
|
const tags = body.tags;
|
|
125
167
|
// SHAPE first: a hand-written spec can put anything under `security.tags`. The server would refuse a
|
|
126
168
|
// malformed value too (its ZodError catch answers `400 Invalid request data`), so this is not closing
|
|
@@ -181,11 +223,14 @@ function resolveLandingBody(opts, cfgLanding) {
|
|
|
181
223
|
}
|
|
182
224
|
return b;
|
|
183
225
|
}
|
|
184
|
-
|
|
226
|
+
// resolved FIRST so `--landing-context X --landing-context-file Y` names the real mistake
|
|
227
|
+
// rather than being masked by the missing-template-id error below
|
|
228
|
+
const context = resolveLandingContext(opts.landingContext, opts.landingContextFile);
|
|
229
|
+
if (opts.landingTemplateId === undefined && context === undefined)
|
|
185
230
|
return null;
|
|
186
231
|
const b = { landing_page_template_id: requireCreateTemplateId(opts.landingTemplateId) };
|
|
187
|
-
if (
|
|
188
|
-
b.landing_page_context =
|
|
232
|
+
if (context !== undefined)
|
|
233
|
+
b.landing_page_context = context;
|
|
189
234
|
return b;
|
|
190
235
|
}
|
|
191
236
|
// ── create collision policy (PIT-7521) ──────────────────────────────────────
|
|
@@ -369,14 +414,37 @@ function clearedListFields(body) {
|
|
|
369
414
|
// that is a warning here and a hard failure in the --if-exists preflight.
|
|
370
415
|
async function readStoredAuthStrategy(client, id) {
|
|
371
416
|
try {
|
|
372
|
-
const
|
|
373
|
-
const strategy =
|
|
417
|
+
const data = (await fetchShareConfig(client, id));
|
|
418
|
+
const strategy = data?.auth_strategy;
|
|
374
419
|
return typeof strategy === 'string' ? strategy : null;
|
|
375
420
|
}
|
|
376
421
|
catch {
|
|
377
422
|
return null;
|
|
378
423
|
}
|
|
379
424
|
}
|
|
425
|
+
// ── read a share back (PIT-7521 QA Q5) ──────────────────────────────────────
|
|
426
|
+
// The only readback the platform offers. The body is returned verbatim — the CLI reshapes nothing,
|
|
427
|
+
// so what the caller sees is exactly what the sharing page is served: the ACCESS CONFIG only —
|
|
428
|
+
// auth_strategy, allowed_emails / allowed_domains, valid_until, allow_forward (+ allow_forward_raw,
|
|
429
|
+
// the rep's own toggle before the admin setting is applied), the landing binding
|
|
430
|
+
// (landing_page_template_id / _snapshot / landing_page_context) and created_at / modified_at.
|
|
431
|
+
// There is NO canvas or file field: the route selects none (see next-core
|
|
432
|
+
// `src/app/api/shared-links/[id]/config/route.ts`), so use the id from `dsr create` for those.
|
|
433
|
+
//
|
|
434
|
+
// ONE id failure mode, not two. The route has no id validation and no 400 path: it matches
|
|
435
|
+
// `pitcher_sharedlink.id` (a varchar) and answers 404 on zero rows, so ANY id that does not match
|
|
436
|
+
// a share — malformed or merely unknown — comes back 404 → NOT_FOUND via `toCliError`.
|
|
437
|
+
export async function runDsrGet(opts, deps = {}) {
|
|
438
|
+
try {
|
|
439
|
+
if (!opts.id)
|
|
440
|
+
throw createCliError('VALIDATION', '--id is required for dsr get');
|
|
441
|
+
const client = clientFor(opts, deps);
|
|
442
|
+
return ok(await fetchShareConfig(client, opts.id));
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
return fail(toCliError(e));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
380
448
|
export function assembleUpdateBody(opts) {
|
|
381
449
|
const cleared = normalizeClearFields(opts.clear);
|
|
382
450
|
assertNoEmptyLists(opts);
|
|
@@ -449,8 +517,40 @@ export async function runDsrDelete(opts, deps = {}) {
|
|
|
449
517
|
return fail(toCliError(e));
|
|
450
518
|
}
|
|
451
519
|
}
|
|
520
|
+
// PIT-7521 QA Q8: the spec's field names were documented only in the README, so a user reading
|
|
521
|
+
// `--help` had to guess the ShareBox source keys. The keys are the wire (snake_case) names.
|
|
522
|
+
const CREATE_HELP = `
|
|
523
|
+
--config-file spec (start from \`p dsr scaffold\`). Keys are the wire (snake_case)
|
|
524
|
+
names; source and security are spread into the create body as written.
|
|
525
|
+
source exactly one of:
|
|
526
|
+
canvas_id existing canvas ULID
|
|
527
|
+
sharebox_items [{ "file_id": "<ULID>", "page_index"?: 0 }] — builds a
|
|
528
|
+
new ShareBox canvas; page_index is optional (omit it
|
|
529
|
+
to share the whole file)
|
|
530
|
+
ShareBox-only extras: instance_id (required unless --instance-id),
|
|
531
|
+
canvas_name, account_id, template_id
|
|
532
|
+
security auth_strategy, allowed_emails [], allowed_domains [], valid_until,
|
|
533
|
+
allow_forward, allow_download, tags [] (tags: ShareBox only)
|
|
534
|
+
allow_download is stored on the CANVAS: omit it (the scaffold does) to
|
|
535
|
+
leave the canvas unchanged; unset, the viewer offers downloads
|
|
536
|
+
landing landing_page_template_id, landing_page_context {}
|
|
537
|
+
Example ShareBox source:
|
|
538
|
+
"source": { "instance_id": "01...", "sharebox_items": [{ "file_id": "01..." }] }`;
|
|
539
|
+
const GET_HELP = `
|
|
540
|
+
Reads the share back through the same public pre-auth config the sharing page
|
|
541
|
+
itself loads (the protected shared-links surface has no GET). The body is
|
|
542
|
+
returned unchanged, and it is the ACCESS CONFIG only: auth_strategy,
|
|
543
|
+
allowed_emails / allowed_domains, valid_until, allow_forward (plus
|
|
544
|
+
allow_forward_raw, the rep's own toggle), the landing-page binding
|
|
545
|
+
(landing_page_template_id, landing_page_template_snapshot,
|
|
546
|
+
landing_page_context) and created_at / modified_at. The endpoint returns NO
|
|
547
|
+
canvas or file — keep the id \`dsr create\` handed back for those.
|
|
548
|
+
--id the shared link ID (required)
|
|
549
|
+
Any id that matches no share answers 404 -> NOT_FOUND (exit 5). That is the
|
|
550
|
+
only id failure mode: the route does not validate the id's format, so a
|
|
551
|
+
malformed id is a miss like any other, not a 400.`;
|
|
452
552
|
export function createDsrCommand() {
|
|
453
|
-
const cmd = new Command('dsr').description('create and share a Digital Sales Room (a canvas share link)');
|
|
553
|
+
const cmd = new Command('dsr').description('create, read and share a Digital Sales Room (a canvas share link)');
|
|
454
554
|
withAuthFlags(cmd.command('scaffold'))
|
|
455
555
|
.description('print a {source,security,landing} share spec to fill in and pass to `dsr create --config-file`')
|
|
456
556
|
.option('--out <path>', 'Write the scaffold to a file instead of stdout')
|
|
@@ -472,17 +572,20 @@ export function createDsrCommand() {
|
|
|
472
572
|
.option('--valid-until <iso>', 'Expiry (ISO datetime)')
|
|
473
573
|
.option('--allow-forward', 'Allow recipients to forward')
|
|
474
574
|
.option('--no-allow-forward', 'Disallow forwarding')
|
|
475
|
-
.option('--allow-download', 'Allow download')
|
|
476
|
-
.option('--no-allow-download', 'Disallow download')
|
|
575
|
+
.option('--allow-download', 'Allow download (with --canvas-id you must own the canvas or hold canvas.update on it, else AUTH)')
|
|
576
|
+
.option('--no-allow-download', 'Disallow download (same ownership rule as --allow-download)')
|
|
477
577
|
.option('--tags <csv>', 'Tags for the ShareBox-built canvas (rejected with --canvas-id, which ignores them)', parseTags)
|
|
478
578
|
.option('--landing-template-id <id>', 'Landing template to bind (optional)')
|
|
479
579
|
.option('--landing-context <json>', 'Landing personalization merge fields (JSON object)')
|
|
480
|
-
.option('--
|
|
580
|
+
.option('--landing-context-file <path>', 'Path to a JSON file with the landing merge fields — the Windows-safe alternative to --landing-context (exactly one of the two)')
|
|
581
|
+
.option('--config-file <path>', 'Full {source,security,landing} spec JSON — source is {canvas_id} or {sharebox_items: [{file_id, page_index?}], ' +
|
|
582
|
+
'instance_id}; field list below (mutually exclusive with the flags above)')
|
|
481
583
|
.option('--if-exists <mode>', 'return|error. What to do when the canvas is already shared: `return` hands back the existing link (default ' +
|
|
482
584
|
'with individual flags), `error` refuses and points at `dsr update` (default with --config-file, whose ' +
|
|
483
585
|
'security block is sent as-is and would overwrite the existing share). The check is a preflight lookup, so ' +
|
|
484
586
|
'it is advisory rather than atomic — a link created between the check and the share is still returned. ' +
|
|
485
587
|
'Never applies to a ShareBox source, which always mints a new canvas.')
|
|
588
|
+
.addHelpText('after', CREATE_HELP)
|
|
486
589
|
.action(async (options, command) => {
|
|
487
590
|
const globals = command.optsWithGlobals();
|
|
488
591
|
emitEnvelope(await runDsrCreate({ ...options, quiet: globals.quiet }), globals);
|
|
@@ -492,11 +595,20 @@ export function createDsrCommand() {
|
|
|
492
595
|
.option('--id <id>', 'Shared link ID (required)')
|
|
493
596
|
.option('--landing-template-id <id>', "Landing template ID, or 'none' to unbind (required)")
|
|
494
597
|
.option('--landing-context <json>', 'Personalization merge fields (JSON object)')
|
|
598
|
+
.option('--landing-context-file <path>', 'Path to a JSON file with the merge fields — the Windows-safe alternative to --landing-context (exactly one of the two)')
|
|
495
599
|
.option('--instance-id <id>', 'Instance ID (required — the endpoint resolves the instance from it)')
|
|
496
600
|
.action(async (options, command) => {
|
|
497
601
|
const globals = command.optsWithGlobals();
|
|
498
602
|
emitEnvelope(await runDsrBindLanding({ ...options, quiet: globals.quiet }), globals);
|
|
499
603
|
});
|
|
604
|
+
withAuthFlags(cmd.command('get'))
|
|
605
|
+
.description('read an existing share back: gating, allow-lists, expiry, forwarding and the bound landing page')
|
|
606
|
+
.option('--id <id>', 'Shared link ID (required)')
|
|
607
|
+
.addHelpText('after', GET_HELP)
|
|
608
|
+
.action(async (options, command) => {
|
|
609
|
+
const globals = command.optsWithGlobals();
|
|
610
|
+
emitEnvelope(await runDsrGet({ ...options, quiet: globals.quiet }), globals);
|
|
611
|
+
});
|
|
500
612
|
// Every --x is declared BEFORE its --no-x on purpose: commander takes a boolean's default
|
|
501
613
|
// from whichever of the pair it sees first, so the reverse order would make the option
|
|
502
614
|
// default to `true` and every call would ship it. For allow_forward that clobbers a rep's
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { AxiosInstance } from 'axios';
|
|
3
|
+
import { type Credential } from '../core/credential.js';
|
|
3
4
|
import { type Envelope } from '../core/envelope.js';
|
|
4
5
|
interface PublishResult {
|
|
5
6
|
app?: {
|
|
@@ -14,6 +15,7 @@ interface PublishResult {
|
|
|
14
15
|
interface PublishOptions {
|
|
15
16
|
folder?: string;
|
|
16
17
|
apiKey?: string;
|
|
18
|
+
credential?: Credential;
|
|
17
19
|
org?: string;
|
|
18
20
|
url?: string;
|
|
19
21
|
incrementVersion?: boolean;
|
|
@@ -4,7 +4,7 @@ import os from 'os';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import FormData from 'form-data';
|
|
7
|
-
import {
|
|
7
|
+
import { credentialOf } from '../core/credential.js';
|
|
8
8
|
import { resolveBaseUrl } from '../core/baseUrl.js';
|
|
9
9
|
import { makeClient, toCliError } from '../core/http.js';
|
|
10
10
|
import { zipDirectoryToFile } from '../core/archive.js';
|
|
@@ -92,7 +92,7 @@ async function installToInstances(client, appName, instances, quiet) {
|
|
|
92
92
|
}
|
|
93
93
|
export async function runPublishApp(opts, deps = {}) {
|
|
94
94
|
try {
|
|
95
|
-
const
|
|
95
|
+
const cred = credentialOf(opts);
|
|
96
96
|
const baseUrl = resolveBaseUrl({ org: opts.org, url: opts.url });
|
|
97
97
|
const folderPath = path.resolve(opts.folder ?? '.');
|
|
98
98
|
const appJsonPath = path.join(folderPath, 'app.json');
|
|
@@ -124,7 +124,7 @@ export async function runPublishApp(opts, deps = {}) {
|
|
|
124
124
|
fs.writeFileSync(appJsonPath, JSON.stringify(existingAppJson, null, 2));
|
|
125
125
|
log(`Updated app.json version to ${nextVersion}`, { quiet: opts.quiet });
|
|
126
126
|
}
|
|
127
|
-
const client = deps.client ?? makeClient(baseUrl,
|
|
127
|
+
const client = deps.client ?? makeClient(baseUrl, cred);
|
|
128
128
|
const zipPath = path.join(deps.tmpDir ?? os.tmpdir(), `cli-publish-${process.pid}-${randomUUID()}.zip`);
|
|
129
129
|
try {
|
|
130
130
|
log(`Zipping ${folderPath}...`, { quiet: opts.quiet });
|
|
@@ -272,9 +272,14 @@ const METADATA_TEMPLATE_SCAFFOLD_DATA = [
|
|
|
272
272
|
export const METADATA_TEMPLATE_SCAFFOLD_JSON = JSON.stringify(METADATA_TEMPLATE_SCAFFOLD_DATA, null, 2);
|
|
273
273
|
const DSR_SHARE_SCAFFOLD_DATA = {
|
|
274
274
|
source: {
|
|
275
|
-
// provide ONE
|
|
275
|
+
// provide ONE source: canvas_id (share an existing canvas) OR a non-empty sharebox_items (build a
|
|
276
|
+
// new ShareBox canvas from files) — delete the canvas_id line when you fill sharebox_items.
|
|
277
|
+
// Emitted as a real key, not a comment, because comments never reach the printed JSON (PIT-7521 QA Q8);
|
|
278
|
+
// an EMPTY array asks for nothing, so the untouched scaffold still shares canvas_id.
|
|
276
279
|
canvas_id: '01EXAMPLECANVASID000000000',
|
|
277
|
-
|
|
280
|
+
sharebox_items: [], // [{ "file_id": "<file ULID>", "page_index"?: 0 }] — page_index optional (one page, not the whole file)
|
|
281
|
+
// ShareBox-only, and NOT emitted because the server refuses null for them — add them when needed:
|
|
282
|
+
// instance_id: '01EXAMPLEINSTANCEID0000000', (required for sharebox_items unless --instance-id is passed)
|
|
278
283
|
// canvas_name: 'Acme QBR',
|
|
279
284
|
// account_id: '01EXAMPLEACCOUNTID00000000',
|
|
280
285
|
// template_id: '01EXAMPLECANVASTEMPLATE0000',
|
|
@@ -285,7 +290,10 @@ const DSR_SHARE_SCAFFOLD_DATA = {
|
|
|
285
290
|
allowed_domains: [],
|
|
286
291
|
valid_until: null,
|
|
287
292
|
allow_forward: true,
|
|
288
|
-
allow_download: false,
|
|
293
|
+
// allow_download: false, — NOT emitted. It is stored on the CANVAS (read by every link on it), and
|
|
294
|
+
// the server writes it only when present: omitted, an existing canvas keeps its value and a new
|
|
295
|
+
// ShareBox canvas carries none, which the viewer treats as downloads allowed. Emitting a default
|
|
296
|
+
// here would silently flip an existing canvas's setting on every untouched run (PIT-7521 review).
|
|
289
297
|
tags: [],
|
|
290
298
|
},
|
|
291
299
|
landing: {
|
package/dist/src/core/archive.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ZipArchive } from 'archiver';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
export async function zipDirectoryToBuffer(dir) {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
5
|
const chunks = [];
|
|
6
|
-
const archive =
|
|
6
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
7
7
|
archive.on('data', (chunk) => chunks.push(chunk));
|
|
8
8
|
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
9
9
|
archive.on('error', reject);
|
|
@@ -14,7 +14,7 @@ export async function zipDirectoryToBuffer(dir) {
|
|
|
14
14
|
export async function zipDirectoryToFile(dir, outPath) {
|
|
15
15
|
return new Promise((resolve, reject) => {
|
|
16
16
|
const output = fs.createWriteStream(outPath);
|
|
17
|
-
const archive =
|
|
17
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
18
18
|
output.on('close', () => resolve(archive.pointer()));
|
|
19
19
|
output.on('error', reject);
|
|
20
20
|
archive.on('error', reject);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credential a request is sent with. `apiKey` is the Knox key the CLI (and stdio `p mcp`) has
|
|
3
|
+
* always used; `bearer` is a user's Auth0 access token, used by the hosted MCP server (PIT-8912).
|
|
4
|
+
*/
|
|
5
|
+
export type Credential = {
|
|
6
|
+
kind: 'bearer';
|
|
7
|
+
token: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'apiKey';
|
|
10
|
+
key: string;
|
|
11
|
+
};
|
|
12
|
+
/** The auth header(s) for a credential. Exactly one header, never both. */
|
|
13
|
+
export declare function authHeaders(c: Credential): Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* The ONE credential resolver every call site uses.
|
|
16
|
+
*
|
|
17
|
+
* An explicit `credential` always wins and never falls through: a bearer context must fail CLOSED,
|
|
18
|
+
* so it can never pick up `--api-key` or `NG_API_KEY` from the process env (the hosted server
|
|
19
|
+
* would otherwise act as whoever minted that key). Only when no credential is given (CLI / stdio)
|
|
20
|
+
* does this reach `resolveApiKey`, which reads `--api-key` then `NG_API_KEY`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function credentialOf(opts: {
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
credential?: Credential;
|
|
25
|
+
}): Credential;
|