@north-light/api 0.3.589 → 0.3.590
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/dist/api/client.d.ts +4 -1
- package/dist/api/client.js +2 -1
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/docs.d.ts +5 -0
- package/dist/api/dto/settings.d.ts +21 -0
- package/dist/build-root.d.ts +2 -0
- package/dist/build-root.js +6 -1
- package/dist/commands/pkg/app-installs.d.ts +53 -0
- package/dist/commands/pkg/app-installs.js +125 -0
- package/dist/commands/pkg/market-inspect.js +4 -0
- package/dist/commands/pkg/market-manage.js +6 -0
- package/dist/commands/pkg/plugin-inspect.js +6 -2
- package/dist/commands/pkg/plugin-manage.js +15 -0
- package/dist/core/canvas/schema-version.d.ts +1 -1
- package/dist/core/canvas/schema-version.js +1 -1
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +10 -0
- package/dist/core/graph/names.js +7 -6
- package/dist/core/graph/package-docs.d.ts +6 -15
- package/dist/core/graph/package-docs.js +33 -83
- package/dist/core/graph/package-publish.d.ts +60 -0
- package/dist/core/graph/package-publish.js +158 -0
- package/dist/core/graph/package-reconcile.d.ts +20 -1
- package/dist/core/graph/package-reconcile.js +83 -15
- package/dist/core/import/export/dump.js +12 -36
- package/dist/core/installed-plugins.js +5 -7
- package/dist/core/packages/eligibility.d.ts +16 -0
- package/dist/core/packages/eligibility.js +34 -0
- package/dist/core/packages/launch-kinds.d.ts +6 -0
- package/dist/core/packages/launch-kinds.js +20 -0
- package/dist/core/packages/source-url.d.ts +3 -0
- package/dist/core/packages/source-url.js +17 -0
- package/dist/core/permissions/ceiling.d.ts +9 -2
- package/dist/core/permissions/ceiling.js +23 -9
- package/dist/core/permissions/evaluator.js +1 -1
- package/dist/core/providers/bundles.js +2 -3
- package/dist/core/runtime/launch-target.d.ts +6 -2
- package/dist/core/runtime/launch-target.js +8 -4
- package/dist/core/runtime/launch.d.ts +8 -1
- package/dist/core/runtime/launch.js +13 -6
- package/dist/core/runtime/spawn.js +5 -5
- package/dist/core/runtime/warm-pool.js +1 -0
- package/dist/core/scoped-state/legacy-profiles.js +1 -1
- package/dist/core/scoped-state/person-rows.js +38 -35
- package/dist/core/scoped-state/person-store.d.ts +23 -0
- package/dist/core/scoped-state/person-store.js +5 -1
- package/package.json +1 -1
- package/dist/core/graph/package-kinds.d.ts +0 -22
- package/dist/core/graph/package-kinds.js +0 -49
- package/dist/core/scoped-state/materialized-plugins.d.ts +0 -20
- package/dist/core/scoped-state/materialized-plugins.js +0 -105
package/dist/api/client.d.ts
CHANGED
|
@@ -340,7 +340,10 @@ export declare class CrtrClient {
|
|
|
340
340
|
/** Package (builtin and plugin) documents. */
|
|
341
341
|
readonly documents: {
|
|
342
342
|
browserAccess: () => Promise<DocumentAccessDTO>;
|
|
343
|
-
|
|
343
|
+
/** `plugins` names the installs a cloud role resolves (a taken name then answers `409 name_taken`); omitted, it resolves all of the app's. */
|
|
344
|
+
reconcilePackages: (opts?: {
|
|
345
|
+
plugins?: string[];
|
|
346
|
+
}) => Promise<ReconcilePackagesDTO>;
|
|
344
347
|
};
|
|
345
348
|
/** A plugin's `crtr_app_` credential and the headers crtrd adds to its local HTTP calls. */
|
|
346
349
|
readonly plugins: {
|
package/dist/api/client.js
CHANGED
|
@@ -620,7 +620,8 @@ export class CrtrClient {
|
|
|
620
620
|
/** Package (builtin and plugin) documents. */
|
|
621
621
|
documents = {
|
|
622
622
|
browserAccess: () => this.request('GET', routes.documentsBrowserAccess()),
|
|
623
|
-
|
|
623
|
+
/** `plugins` names the installs a cloud role resolves (a taken name then answers `409 name_taken`); omitted, it resolves all of the app's. */
|
|
624
|
+
reconcilePackages: (opts) => this.request('POST', routes.documentsReconcilePackages(), opts),
|
|
624
625
|
};
|
|
625
626
|
/** A plugin's `crtr_app_` credential and the headers crtrd adds to its local HTTP calls. */
|
|
626
627
|
plugins = {
|
package/dist/api/contract.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
2
|
-
export declare const CONTRACT_LEVEL = "1.
|
|
2
|
+
export declare const CONTRACT_LEVEL = "1.44";
|
|
3
3
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
4
4
|
export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
|
|
5
5
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
package/dist/api/contract.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// PURITY (spec §3.1): Node built-ins + `src/api/*` only.
|
|
14
14
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
15
|
-
export const CONTRACT_LEVEL = '1.
|
|
15
|
+
export const CONTRACT_LEVEL = '1.44';
|
|
16
16
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
17
17
|
export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
|
|
18
18
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
package/dist/api/dto/docs.d.ts
CHANGED
|
@@ -173,6 +173,11 @@ export interface ReconcilePackagesDTO {
|
|
|
173
173
|
error: string;
|
|
174
174
|
}>;
|
|
175
175
|
};
|
|
176
|
+
/** cloud role: installed plugins whose documents are not on the canvas — `not_public` (the directory reads public marketplaces only), `not_found` (the directory does not list it) or `directory_unreachable` (the next sync publishes them) */
|
|
177
|
+
unavailable?: Array<{
|
|
178
|
+
plugin: string;
|
|
179
|
+
reason: 'not_public' | 'not_found' | 'directory_unreachable';
|
|
180
|
+
}>;
|
|
176
181
|
/** the `crtr_app_` tokens after this pass: who holds one (with its scopes) and whose was removed */
|
|
177
182
|
tokens?: {
|
|
178
183
|
granted: Array<{
|
|
@@ -23,12 +23,33 @@ export interface PersonSettingsSnapshotDTO {
|
|
|
23
23
|
/** Owner key (`user`, `profile:<id>`) → raw settings, with the `plugins` and `marketplaces` blocks rebuilt from their rows. */
|
|
24
24
|
settings: Record<string, Record<string, unknown>>;
|
|
25
25
|
state: Record<string, SettingsScopeStateDTO>;
|
|
26
|
+
/** App id (`app:<id>`) → that app's plugin and marketplace installs; `revision` covers only that block. */
|
|
27
|
+
apps: Record<string, SettingsAppInstallsDTO>;
|
|
28
|
+
}
|
|
29
|
+
export interface SettingsAppInstallsDTO {
|
|
30
|
+
plugins: Record<string, {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
source_marketplace?: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
}>;
|
|
35
|
+
marketplaces: Record<string, {
|
|
36
|
+
url?: string;
|
|
37
|
+
ref?: string;
|
|
38
|
+
installed_at?: string;
|
|
39
|
+
}>;
|
|
40
|
+
revision: string;
|
|
26
41
|
}
|
|
27
42
|
export type SettingsOpDTO = {
|
|
28
43
|
op: 'settings.replace';
|
|
29
44
|
owner: string;
|
|
30
45
|
raw: Record<string, unknown>;
|
|
31
46
|
ifRevision: string;
|
|
47
|
+
} | {
|
|
48
|
+
op: 'app-plugins.replace';
|
|
49
|
+
app: string;
|
|
50
|
+
plugins: SettingsAppInstallsDTO['plugins'];
|
|
51
|
+
marketplaces: SettingsAppInstallsDTO['marketplaces'];
|
|
52
|
+
ifRevision: string;
|
|
32
53
|
} | {
|
|
33
54
|
op: 'state.write';
|
|
34
55
|
owner: string;
|
package/dist/build-root.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare function personOnlyInvocation(argv: readonly string[]): string |
|
|
|
23
23
|
/** Select a smaller canonical tree; `runCli` still parses, renders help, and reports errors. */
|
|
24
24
|
export declare function isDaemonControlInvocation(argv: readonly string[]): boolean;
|
|
25
25
|
/** `sys signin` and `sys signout` run when the stored sign-in is gone or a move is unfinished, the states in which the settings load would refuse. */
|
|
26
|
+
/** `pkg plugin …` and `pkg market …` read and write the calling node's app install block, so a sandboxed node loads the settings snapshot for them. */
|
|
27
|
+
export declare function isPkgInstallInvocation(argv: readonly string[]): boolean;
|
|
26
28
|
export declare function isSignInInvocation(argv: readonly string[]): boolean;
|
|
27
29
|
export declare function isJoinInvocation(argv: readonly string[]): boolean;
|
|
28
30
|
export declare function isLayoutMigrationInvocation(argv: readonly string[]): boolean;
|
package/dist/build-root.js
CHANGED
|
@@ -268,7 +268,7 @@ const PERSON_ONLY_PATHS = [
|
|
|
268
268
|
['canvas', 'chord'], ['canvas', 'config'], ['canvas', 'use'], ['canvas', 'browse'], ['canvas', 'issue'],
|
|
269
269
|
];
|
|
270
270
|
/** Leaves inside a person-only subtree that an app's node may still run: the command a node runs when the harness itself misbehaves. */
|
|
271
|
-
const NODE_CALLABLE_PATHS = [['sys', 'feedback']];
|
|
271
|
+
const NODE_CALLABLE_PATHS = [['sys', 'feedback'], ['pkg', 'plugin'], ['pkg', 'market']];
|
|
272
272
|
function startsWithPath(tokens, path) {
|
|
273
273
|
return path.every((name, index) => tokens[index] === name);
|
|
274
274
|
}
|
|
@@ -283,6 +283,11 @@ export function personOnlyInvocation(argv) {
|
|
|
283
283
|
/** Select a smaller canonical tree; `runCli` still parses, renders help, and reports errors. */
|
|
284
284
|
export function isDaemonControlInvocation(argv) { return isSysInvocation(argv, 'daemon'); }
|
|
285
285
|
/** `sys signin` and `sys signout` run when the stored sign-in is gone or a move is unfinished, the states in which the settings load would refuse. */
|
|
286
|
+
/** `pkg plugin …` and `pkg market …` read and write the calling node's app install block, so a sandboxed node loads the settings snapshot for them. */
|
|
287
|
+
export function isPkgInstallInvocation(argv) {
|
|
288
|
+
const tokens = argv.slice(2).filter((token) => !GLOBAL_TOKENS.has(token));
|
|
289
|
+
return tokens[0] === 'pkg' && (tokens[1] === 'plugin' || tokens[1] === 'market');
|
|
290
|
+
}
|
|
286
291
|
export function isSignInInvocation(argv) { return isSysInvocation(argv, 'signin') || isSysInvocation(argv, 'signout'); }
|
|
287
292
|
export function isJoinInvocation(argv) { return isSysInvocation(argv, 'join'); }
|
|
288
293
|
export function isLayoutMigrationInvocation(argv) { return isSysInvocation(argv, 'migrate'); }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const appId: () => string;
|
|
2
|
+
export declare function installAppPlugin(ref: string): Promise<{
|
|
3
|
+
name: string;
|
|
4
|
+
app: string;
|
|
5
|
+
warnings?: string[];
|
|
6
|
+
}>;
|
|
7
|
+
export declare function removeAppPlugin(name: string): Promise<{
|
|
8
|
+
app: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function setAppPluginEnabled(name: string, enabled: boolean): Promise<{
|
|
11
|
+
name: string;
|
|
12
|
+
app: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
warnings?: string[];
|
|
15
|
+
}>;
|
|
16
|
+
/** Refresh the documents of one install, or all of the app's: the directory re-reads its marketplaces hourly and the control plane converges on its next sync. */
|
|
17
|
+
export declare function updateAppPlugins(name: string | undefined): Promise<{
|
|
18
|
+
updated: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
warnings?: string[];
|
|
21
|
+
}>;
|
|
22
|
+
}>;
|
|
23
|
+
/** The app's installs. Each says why it has no documents when the directory cannot serve it (a name another source holds, a private marketplace, a directory that is down). */
|
|
24
|
+
export declare function listAppPlugins(includeDisabled: boolean): Promise<{
|
|
25
|
+
items: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
scope: string;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
source_marketplace?: string;
|
|
30
|
+
version?: string;
|
|
31
|
+
documents?: string;
|
|
32
|
+
}>;
|
|
33
|
+
next_cursor: null;
|
|
34
|
+
total: number;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function addAppMarketplace(url: string, ref: string | undefined): Promise<{
|
|
37
|
+
name: string;
|
|
38
|
+
app: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** The marketplaces the app has registered, in the shape `pkg market list` prints (no clone path: a sandbox holds none). */
|
|
41
|
+
export declare function listAppMarketplaces(): {
|
|
42
|
+
items: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
scope: string;
|
|
45
|
+
url: string | undefined;
|
|
46
|
+
ref: string | undefined;
|
|
47
|
+
}>;
|
|
48
|
+
next_cursor: null;
|
|
49
|
+
total: number;
|
|
50
|
+
};
|
|
51
|
+
export declare function removeAppMarketplace(name: string): Promise<{
|
|
52
|
+
app: string;
|
|
53
|
+
}>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `crtr pkg plugin …` and `crtr pkg market …` in a sandboxed node: the node's plugins are its app's install block on the control plane
|
|
2
|
+
// (`app-plugins.replace`), not a directory on disk. The sandbox clones the plugin code itself at its next start; the documents come from the
|
|
3
|
+
// directory through `reconcile-packages`.
|
|
4
|
+
import { ApiError } from '../../api/errors.js';
|
|
5
|
+
import { envGrantee } from '../../shared/env.js';
|
|
6
|
+
import { deriveNameFromUrl } from '../../core/git.js';
|
|
7
|
+
import { notFound, usage, CrtrError } from '../../core/errors.js';
|
|
8
|
+
import { nowIso } from '../../core/fs-utils.js';
|
|
9
|
+
import { appRevisionOf, currentSnapshot, writeSettings } from '../../core/scoped-state/person-store.js';
|
|
10
|
+
import { ExitCode } from '../../types.js';
|
|
11
|
+
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
12
|
+
const OFFICIAL = { name: 'crouter-official-marketplace', url: 'https://github.com/crouton-labs/crouter-official-marketplace.git' };
|
|
13
|
+
export const appId = () => envGrantee() ?? 'app:terminal';
|
|
14
|
+
const block = () => currentSnapshot().apps[appId()] ?? { plugins: {}, marketplaces: {}, revision: 'absent' };
|
|
15
|
+
async function change(mutate) {
|
|
16
|
+
const app = appId();
|
|
17
|
+
await writeSettings((snapshot) => {
|
|
18
|
+
const current = snapshot.apps[app];
|
|
19
|
+
const next = structuredClone({ plugins: current?.plugins ?? {}, marketplaces: current?.marketplaces ?? {} });
|
|
20
|
+
mutate(next);
|
|
21
|
+
return [{ op: 'app-plugins.replace', app, plugins: next.plugins, marketplaces: next.marketplaces, ifRevision: appRevisionOf(current) }];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Ask the control plane to resolve the named installs against the directory. A taken name or an unlisted plugin refuses the install: the caller removes its row. */
|
|
25
|
+
async function resolve(plugins) {
|
|
26
|
+
return await resolveRaw(plugins).catch((err) => rethrowAsCliError(err));
|
|
27
|
+
}
|
|
28
|
+
async function resolveRaw(plugins) {
|
|
29
|
+
return await cliClient().documents.reconcilePackages({ plugins });
|
|
30
|
+
}
|
|
31
|
+
/** Resolve all of the app's installs: a taken or unlisted name is reported, never refused. */
|
|
32
|
+
async function resolveAll() {
|
|
33
|
+
return await cliClient().documents.reconcilePackages().catch((err) => rethrowAsCliError(err));
|
|
34
|
+
}
|
|
35
|
+
function unavailableWarnings(report, name) {
|
|
36
|
+
const why = { not_public: 'installed without documents: the directory reads public marketplaces only', not_found: 'installed without documents: the directory does not list it', directory_unreachable: 'installed; its documents arrive on the next sync because the directory is unreachable' };
|
|
37
|
+
const unavailable = (report.unavailable ?? []).filter((u) => name === undefined || u.plugin === name).map((u) => `${u.plugin}: ${why[u.reason]}`);
|
|
38
|
+
// A published package can be partial: files that did not parse (paths start with the plugin's name) and kinds another owner holds.
|
|
39
|
+
const invalid = report.invalid.filter((i) => name === undefined || i.path.startsWith(`${name}/`)).map((i) => `${i.path.split('/')[0]}: ${i.path} was not published: ${i.error}`);
|
|
40
|
+
const refused = (report.kinds?.refused ?? []).filter((k) => name === undefined || k.plugin === name).map((k) => `${k.plugin}: kind ${k.kind} not registered: ${k.message}`);
|
|
41
|
+
return [...unavailable, ...invalid, ...refused];
|
|
42
|
+
}
|
|
43
|
+
export async function installAppPlugin(ref) {
|
|
44
|
+
const parts = ref.split('/');
|
|
45
|
+
if (parts.length !== 2 || parts[0] === '' || parts[1] === '')
|
|
46
|
+
throw usage(`invalid plugin install ref: ${ref}\nA sandboxed node installs marketplace/plugin from a marketplace added with \`crtr pkg market add\`.`);
|
|
47
|
+
const [market, name] = parts;
|
|
48
|
+
const before = block();
|
|
49
|
+
if (before.plugins[name] !== undefined)
|
|
50
|
+
throw new CrtrError('conflict', `plugin "${name}" is already installed in ${appId()}`, ExitCode.GENERAL);
|
|
51
|
+
const known = before.marketplaces[market] ?? (market === OFFICIAL.name ? { url: OFFICIAL.url, ref: 'main', installed_at: nowIso() } : undefined);
|
|
52
|
+
if (known === undefined)
|
|
53
|
+
throw notFound(`marketplace not found: ${market}`);
|
|
54
|
+
await change((b) => { b.marketplaces[market] = known; b.plugins[name] = { enabled: true, source_marketplace: market }; });
|
|
55
|
+
try {
|
|
56
|
+
const report = await resolveRaw([name]);
|
|
57
|
+
const warnings = unavailableWarnings(report, name);
|
|
58
|
+
return { name, app: appId(), ...(warnings.length > 0 ? { warnings } : {}) };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Only the directory's `name_taken` and `not_found` refuse the install (the row was written ahead of its answer); anything else leaves it standing.
|
|
62
|
+
if (!(error instanceof ApiError) || (error.code !== 'name_taken' && error.code !== 'not_found'))
|
|
63
|
+
return rethrowAsCliError(error);
|
|
64
|
+
await change((b) => { delete b.plugins[name]; if (before.marketplaces[market] === undefined && !Object.values(b.plugins).some((p) => p.source_marketplace === market))
|
|
65
|
+
delete b.marketplaces[market]; });
|
|
66
|
+
if (error.code === 'not_found')
|
|
67
|
+
throw notFound(error.message);
|
|
68
|
+
throw new CrtrError('conflict', `${error.message}; uninstall that marketplace's plugin or pick another`, ExitCode.GENERAL, { plugin: name });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function removeAppPlugin(name) {
|
|
72
|
+
if (block().plugins[name] === undefined)
|
|
73
|
+
throw notFound(`plugin not found: ${name}`);
|
|
74
|
+
await change((b) => { delete b.plugins[name]; });
|
|
75
|
+
return { app: appId() };
|
|
76
|
+
}
|
|
77
|
+
export async function setAppPluginEnabled(name, enabled) {
|
|
78
|
+
if (block().plugins[name] === undefined)
|
|
79
|
+
throw notFound(`plugin not found: ${name}`);
|
|
80
|
+
await change((b) => { b.plugins[name] = { ...b.plugins[name], enabled }; });
|
|
81
|
+
const warnings = enabled ? unavailableWarnings(await resolve([name]), name) : [];
|
|
82
|
+
return { name, app: appId(), enabled, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
83
|
+
}
|
|
84
|
+
/** Refresh the documents of one install, or all of the app's: the directory re-reads its marketplaces hourly and the control plane converges on its next sync. */
|
|
85
|
+
export async function updateAppPlugins(name) {
|
|
86
|
+
const installed = Object.keys(block().plugins);
|
|
87
|
+
if (name !== undefined && !installed.includes(name))
|
|
88
|
+
throw notFound(`plugin not found: ${name}`);
|
|
89
|
+
const names = name === undefined ? installed : [name];
|
|
90
|
+
const report = names.length === 0 ? undefined : name === undefined ? await resolveAll() : await resolve(names);
|
|
91
|
+
const conflicted = (n) => (report?.conflicts ?? []).filter((c) => c.plugin === n).map((c) => `${n}: no documents: ${c.message}`);
|
|
92
|
+
return { updated: names.map((n) => { const warnings = report === undefined ? [] : [...conflicted(n), ...unavailableWarnings(report, n)]; return { name: n, ...(warnings.length > 0 ? { warnings } : {}) }; }) };
|
|
93
|
+
}
|
|
94
|
+
/** The app's installs. Each says why it has no documents when the directory cannot serve it (a name another source holds, a private marketplace, a directory that is down). */
|
|
95
|
+
export async function listAppPlugins(includeDisabled) {
|
|
96
|
+
const entries = Object.entries(block().plugins).filter(([, p]) => includeDisabled || p.enabled !== false).sort(([a], [b]) => a.localeCompare(b));
|
|
97
|
+
const enabled = entries.filter(([, p]) => p.enabled !== false).map(([n]) => n);
|
|
98
|
+
const report = enabled.length === 0 ? undefined : await resolveAll();
|
|
99
|
+
const why = (name) => {
|
|
100
|
+
const conflict = report?.conflicts.find((c) => c.plugin === name);
|
|
101
|
+
if (conflict !== undefined)
|
|
102
|
+
return `no documents: ${conflict.message}`;
|
|
103
|
+
return report === undefined ? undefined : unavailableWarnings(report, name)[0];
|
|
104
|
+
};
|
|
105
|
+
const items = entries.map(([name, p]) => ({ name, scope: appId(), enabled: p.enabled !== false, ...(p.source_marketplace === undefined ? {} : { source_marketplace: p.source_marketplace }), ...(p.version === undefined ? {} : { version: p.version }), ...(why(name) === undefined ? {} : { documents: why(name) }) }));
|
|
106
|
+
return { items, next_cursor: null, total: items.length };
|
|
107
|
+
}
|
|
108
|
+
export async function addAppMarketplace(url, ref) {
|
|
109
|
+
const name = deriveNameFromUrl(url);
|
|
110
|
+
await change((b) => { b.marketplaces[name] = { url, ref: ref ?? 'main', installed_at: nowIso() }; });
|
|
111
|
+
return { name, app: appId() };
|
|
112
|
+
}
|
|
113
|
+
/** The marketplaces the app has registered, in the shape `pkg market list` prints (no clone path: a sandbox holds none). */
|
|
114
|
+
export function listAppMarketplaces() {
|
|
115
|
+
const items = Object.entries(block().marketplaces).sort(([a], [b]) => a.localeCompare(b)).map(([name, m]) => ({ name, scope: appId(), url: m.url, ref: m.ref }));
|
|
116
|
+
return { items, next_cursor: null, total: items.length };
|
|
117
|
+
}
|
|
118
|
+
export async function removeAppMarketplace(name) {
|
|
119
|
+
if (block().marketplaces[name] === undefined)
|
|
120
|
+
throw notFound(`marketplace not found: ${name}`);
|
|
121
|
+
await change((b) => { delete b.marketplaces[name]; for (const [p, e] of Object.entries(b.plugins))
|
|
122
|
+
if (e.source_marketplace === name)
|
|
123
|
+
delete b.plugins[p]; });
|
|
124
|
+
return { app: appId() };
|
|
125
|
+
}
|
|
@@ -3,6 +3,8 @@ import { notFound } from '../../core/errors.js';
|
|
|
3
3
|
import { paginate } from '../../core/pagination.js';
|
|
4
4
|
import { listInstalledPlugins, listInstalledMarketplaces, listAllMarketplaces, findMarketplaceByName, } from '../../core/resolver.js';
|
|
5
5
|
import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
6
|
+
import { isSandboxedNode } from '../../shared/env.js';
|
|
7
|
+
import { listAppMarketplaces } from './app-installs.js';
|
|
6
8
|
export const marketList = defineLeaf({
|
|
7
9
|
name: 'list',
|
|
8
10
|
description: 'list registered marketplaces',
|
|
@@ -24,6 +26,8 @@ export const marketList = defineLeaf({
|
|
|
24
26
|
effects: ['None. Read-only.'],
|
|
25
27
|
},
|
|
26
28
|
run: async (input) => {
|
|
29
|
+
if (isSandboxedNode())
|
|
30
|
+
return listAppMarketplaces();
|
|
27
31
|
const scopeInput = input['scope'];
|
|
28
32
|
const limitRaw = input['limit'];
|
|
29
33
|
const limit = limitRaw !== undefined ? Math.min(Math.max(1, limitRaw), 200) : 50;
|
|
@@ -4,6 +4,8 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
4
4
|
import { general, notFound } from '../../core/errors.js';
|
|
5
5
|
import { listAllMarketplaces, listAllPlugins, findMarketplaceByName } from '../../core/resolver.js';
|
|
6
6
|
import { resolveScopeArg, requireScopeRoot, marketplacesDir, pluginsDir } from '../../core/scope.js';
|
|
7
|
+
import { isSandboxedNode } from '../../shared/env.js';
|
|
8
|
+
import { addAppMarketplace, removeAppMarketplace } from './app-installs.js';
|
|
7
9
|
import { updateConfig, updateState, ensureScopeInitialized, readConfig } from '../../core/config.js';
|
|
8
10
|
import { pathExists, ensureDir, removePath, isSymlink, nowIso, readSymlinkTarget } from '../../core/fs-utils.js';
|
|
9
11
|
import { clone, deriveNameFromUrl, currentSha, gitSync, isGitRepo } from '../../core/git.js';
|
|
@@ -72,6 +74,8 @@ export const marketAdd = defineLeaf({
|
|
|
72
74
|
const url = input['url'];
|
|
73
75
|
const ref = input['ref'];
|
|
74
76
|
const scopeInput = input['scope'];
|
|
77
|
+
if (isSandboxedNode())
|
|
78
|
+
return await addAppMarketplace(url, ref);
|
|
75
79
|
return addMarketplace({ url, ref, scope: resolveInstallScope(scopeInput) });
|
|
76
80
|
},
|
|
77
81
|
});
|
|
@@ -92,6 +96,8 @@ export const marketRemove = defineLeaf({
|
|
|
92
96
|
},
|
|
93
97
|
run: async (input) => {
|
|
94
98
|
const name = input['name'];
|
|
99
|
+
if (isSandboxedNode())
|
|
100
|
+
return await removeAppMarketplace(name);
|
|
95
101
|
const scopeInput = input['scope'];
|
|
96
102
|
let targetScope;
|
|
97
103
|
let mktRoot;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { notFound } from '../../core/errors.js';
|
|
3
|
+
import { isSandboxedNode } from '../../shared/env.js';
|
|
4
|
+
import { listAppPlugins } from './app-installs.js';
|
|
3
5
|
import { paginate } from '../../core/pagination.js';
|
|
4
6
|
import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
|
|
5
7
|
import { packageFiles } from '../../core/graph/package-files.js';
|
|
@@ -34,16 +36,18 @@ export const pluginList = defineLeaf({
|
|
|
34
36
|
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
35
37
|
],
|
|
36
38
|
output: [
|
|
37
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, scope, version?, enabled, transport, source_marketplace?, path}. transport is exec or http for command plugins, null for docs-only plugins. Sorted by scope then name ascending.' },
|
|
39
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, scope, version?, enabled, transport, source_marketplace?, path, documents?}. documents, on a sandboxed node, says why an install has no documents. transport is exec or http for command plugins, null for docs-only plugins. Sorted by scope then name ascending.' },
|
|
38
40
|
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
39
41
|
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact when cheap; null otherwise.' },
|
|
40
42
|
],
|
|
41
43
|
outputKind: 'object',
|
|
42
|
-
effects: ['None.
|
|
44
|
+
effects: ['None on a Mac. On a sandboxed node it asks the directory about each enabled install, which publishes what the directory serves.'],
|
|
43
45
|
},
|
|
44
46
|
run: async (input) => {
|
|
45
47
|
const scopeInput = input['scope'];
|
|
46
48
|
const includeDisabled = input['includeDisabled'] ?? false;
|
|
49
|
+
if (isSandboxedNode())
|
|
50
|
+
return await listAppPlugins(includeDisabled);
|
|
47
51
|
const limitRaw = input['limit'];
|
|
48
52
|
const limit = limitRaw !== undefined ? Math.min(Math.max(1, limitRaw), 200) : 50;
|
|
49
53
|
const cursor = input['cursor'];
|
|
@@ -5,6 +5,8 @@ import { dirname, join, resolve, isAbsolute, relative } from 'node:path';
|
|
|
5
5
|
import { renameSync } from 'node:fs';
|
|
6
6
|
import { defineLeaf } from '../../core/command.js';
|
|
7
7
|
import { cliClient, rethrowAsCliError } from '../api-client.js';
|
|
8
|
+
import { isSandboxedNode } from '../../shared/env.js';
|
|
9
|
+
import { installAppPlugin, removeAppPlugin, setAppPluginEnabled, updateAppPlugins } from './app-installs.js';
|
|
8
10
|
import { notFound, usage, general, network, CrtrError } from '../../core/errors.js';
|
|
9
11
|
import { findMarketplaceByName, findPluginByName, listAllPlugins } from '../../core/resolver.js';
|
|
10
12
|
import { pluginsDir, ensureProjectScopeRoot, userScopeRoot, resolveScopeArg, projectScopeRoot, requireScopeRoot, } from '../../core/scope.js';
|
|
@@ -810,6 +812,11 @@ export const pluginInstall = defineLeaf({
|
|
|
810
812
|
const authEnv = input['authEnv'];
|
|
811
813
|
const scopeInput = input['scope'];
|
|
812
814
|
const gitRef = input['ref'];
|
|
815
|
+
if (isSandboxedNode()) {
|
|
816
|
+
if (rawRef === undefined || endpoint !== undefined || name !== undefined || authEnv !== undefined || gitRef !== undefined || scopeInput !== undefined)
|
|
817
|
+
throw usage('a sandboxed node installs only INSTALL-REF as marketplace/plugin');
|
|
818
|
+
return await installAppPlugin(rawRef);
|
|
819
|
+
}
|
|
813
820
|
const scope = resolveInstallScope(scopeInput);
|
|
814
821
|
if ((rawRef === undefined) === (endpoint === undefined)) {
|
|
815
822
|
throw usage('provide exactly one of INSTALL-REF or --endpoint');
|
|
@@ -867,6 +874,8 @@ export const pluginRemove = defineLeaf({
|
|
|
867
874
|
if (name === 'crtr') {
|
|
868
875
|
throw usage('cannot remove builtin plugin "crtr" — it ships with the binary');
|
|
869
876
|
}
|
|
877
|
+
if (isSandboxedNode())
|
|
878
|
+
return await removeAppPlugin(name);
|
|
870
879
|
let scopes;
|
|
871
880
|
if (scopeInput !== undefined) {
|
|
872
881
|
const resolved = resolveScopeArg(scopeInput);
|
|
@@ -930,6 +939,8 @@ export const pluginEnable = defineLeaf({
|
|
|
930
939
|
},
|
|
931
940
|
run: async (input) => {
|
|
932
941
|
const name = input['name'];
|
|
942
|
+
if (isSandboxedNode())
|
|
943
|
+
return await setAppPluginEnabled(name, true);
|
|
933
944
|
const target = resolvePluginConfigTarget(name, input['scope']);
|
|
934
945
|
if (target === null)
|
|
935
946
|
throw notFound(`plugin not found: ${name}`);
|
|
@@ -961,6 +972,8 @@ export const pluginDisable = defineLeaf({
|
|
|
961
972
|
effects: ['Sets plugin enabled=false in config.json.'],
|
|
962
973
|
},
|
|
963
974
|
run: async (input) => {
|
|
975
|
+
if (isSandboxedNode())
|
|
976
|
+
return await setAppPluginEnabled(input['name'], false);
|
|
964
977
|
const result = await setPluginEnabled(input, false);
|
|
965
978
|
await reconcilePluginDocuments();
|
|
966
979
|
return { scope: result['scope'] };
|
|
@@ -989,6 +1002,8 @@ export const pluginUpdate = defineLeaf({
|
|
|
989
1002
|
},
|
|
990
1003
|
run: async (input) => {
|
|
991
1004
|
const name = input['name'];
|
|
1005
|
+
if (isSandboxedNode())
|
|
1006
|
+
return await updateAppPlugins(name);
|
|
992
1007
|
const scopeInput = input['scope'];
|
|
993
1008
|
const scope = scopeInput === undefined ? undefined : resolveScopeArg(scopeInput);
|
|
994
1009
|
const marketplaceCache = new Map();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The canvas schema version this build supports: the number of the highest delta in `schema-pg/` (the baseline
|
|
2
2
|
* `schema-pg.sql` is version 1). `scripts/install-runtime.mjs` reads this module from a candidate generation to refuse
|
|
3
3
|
* one below the live database's `schema_min_reader`. */
|
|
4
|
-
export declare const CANVAS_SCHEMA_VERSION =
|
|
4
|
+
export declare const CANVAS_SCHEMA_VERSION = 11;
|
|
5
5
|
/** The Postgres schema holding the canvas tables: `CRTR_DATABASE_SCHEMA` (`crtr` in the cloud deploy). Unset or empty
|
|
6
6
|
* means the role's own search_path (`public` on a Mac and on a VM). A name that is not a plain identifier is an error:
|
|
7
7
|
* it is put in a `search_path` connection option. */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/** The canvas schema version this build supports: the number of the highest delta in `schema-pg/` (the baseline
|
|
7
7
|
* `schema-pg.sql` is version 1). `scripts/install-runtime.mjs` reads this module from a candidate generation to refuse
|
|
8
8
|
* one below the live database's `schema_min_reader`. */
|
|
9
|
-
export const CANVAS_SCHEMA_VERSION =
|
|
9
|
+
export const CANVAS_SCHEMA_VERSION = 11;
|
|
10
10
|
/** The Postgres schema holding the canvas tables: `CRTR_DATABASE_SCHEMA` (`crtr` in the cloud deploy). Unset or empty
|
|
11
11
|
* means the role's own search_path (`public` on a Mac and on a VM). A name that is not a plain identifier is an error:
|
|
12
12
|
* it is put in a `search_path` connection option. */
|
package/dist/core/config.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export declare function normalizeRequires(raw: unknown): Record<string, string>;
|
|
|
85
85
|
/** STRICT install-time validation for a plugin's advisory `requires` block.
|
|
86
86
|
* A missing executable is advisory; only malformed declarations fail install. */
|
|
87
87
|
export declare function invalidPluginRequiresReasons(raw: unknown): string[];
|
|
88
|
+
export declare function setAppPluginKinds(kinds: Record<string, unknown> | undefined): void;
|
|
88
89
|
/** The ACTIVE page-component catalog every authoring, validation, daemon,
|
|
89
90
|
* inbox, and TUI consumer resolves: the user scope's own `page_components`
|
|
90
91
|
* entries plus the contributions of every ENABLED plugin installed under that
|
package/dist/core/config.js
CHANGED
|
@@ -630,6 +630,10 @@ function mergeKinds(raw, base = defaultKindsConfig()) {
|
|
|
630
630
|
}
|
|
631
631
|
return out;
|
|
632
632
|
}
|
|
633
|
+
/** The launch kinds of the plugins a cloud role's node has enabled in its app, by plugin name (`appLaunchKinds`). A cloud role holds no plugin
|
|
634
|
+
* directories, so when this is set the user scope layers these instead of listing a directory. The launch-config subprocess sets it from its request. */
|
|
635
|
+
let appPluginKinds;
|
|
636
|
+
export function setAppPluginKinds(kinds) { appPluginKinds = kinds; }
|
|
633
637
|
/** Layer the `kinds` contributions of every ENABLED plugin installed under
|
|
634
638
|
* `scopeRootPath` over `base`, plugin name-sorted for determinism (directory
|
|
635
639
|
* listing order is filesystem-dependent). Each plugin's block goes through
|
|
@@ -638,6 +642,12 @@ function mergeKinds(raw, base = defaultKindsConfig()) {
|
|
|
638
642
|
* `readMergedLaunchConfig` BELOW the host scope's own raw `kinds`, so the
|
|
639
643
|
* scope's config.json always overrides its plugins. */
|
|
640
644
|
function layerPluginKinds(scope, scopeRootPath, base) {
|
|
645
|
+
if (scope === 'user' && appPluginKinds !== undefined) {
|
|
646
|
+
let out = base;
|
|
647
|
+
for (const name of Object.keys(appPluginKinds).sort((a, b) => a.localeCompare(b)))
|
|
648
|
+
out = mergeKinds(appPluginKinds[name], out);
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
641
651
|
if (scopeRootPath === null)
|
|
642
652
|
return base;
|
|
643
653
|
const plugins = listInstalledPluginsInRoot(scope, scopeRootPath)
|
package/dist/core/graph/names.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isSafeNodeId } from '../canvas/paths.js';
|
|
2
2
|
import { OBJECT_TYPE } from '../permissions/builtin.js';
|
|
3
|
-
import { ceilingFrag, resolveCeiling } from '../permissions/ceiling.js';
|
|
3
|
+
import { ceilingFrag, readablePackageApps, resolveCeiling } from '../permissions/ceiling.js';
|
|
4
4
|
import { compilePermission } from '../permissions/compile.js';
|
|
5
5
|
import { permits, readerUser, resolveReader } from '../permissions/evaluator.js';
|
|
6
6
|
import { loadDefinitions } from '../permissions/schemas.js';
|
|
@@ -14,11 +14,12 @@ export async function viewOf(db, reader) {
|
|
|
14
14
|
const user = await readerUser(db, reader);
|
|
15
15
|
const person = user === null ? undefined : await db.prepare(`SELECT o.* FROM objects o JOIN identities i ON i.object = o.id AND i.scheme = 'user'
|
|
16
16
|
WHERE o.type = 'person' AND o.home_machine IS NULL AND regexp_replace(i.external_id, '^user:', '') = ?`).get(user);
|
|
17
|
-
// The apps
|
|
18
|
-
const
|
|
17
|
+
// The apps whose shipped documents the reader may read (the read rule), `crtr` last. A person reads every published package.
|
|
18
|
+
const eligible = await readablePackageApps(db, reader, user);
|
|
19
|
+
const tools = await db.prepare(`SELECT a.* FROM objects a WHERE a.type = 'app' AND a.deleted_at IS NULL AND ${eligible === null ? `EXISTS (
|
|
19
20
|
SELECT 1 FROM objects o JOIN documents d ON d.object = o.id
|
|
20
|
-
WHERE o.owner = a.id AND o.type = 'document' AND o.deleted_at IS NULL AND d.source_path IS NOT NULL)
|
|
21
|
-
ORDER BY a.name = 'crtr', a.name`).all();
|
|
21
|
+
WHERE o.owner = a.id AND o.type = 'document' AND o.deleted_at IS NULL AND d.source_path IS NOT NULL)` : 'a.id = ANY(?::text[])'}
|
|
22
|
+
ORDER BY a.name = 'crtr', a.name`).all(...(eligible === null ? [] : [eligible]));
|
|
22
23
|
const head = reader.class === 'node' ? [reader.node, ...reader.repos, ...reader.projectRepos, reader.profile, reader.app]
|
|
23
24
|
: reader.class === 'app' ? [reader.app]
|
|
24
25
|
: reader.class === 'object' ? [reader.owner, ...reader.repos, reader.app]
|
|
@@ -32,7 +33,7 @@ export async function viewOf(db, reader) {
|
|
|
32
33
|
* schema-typed object has had its facts removed, so only the `permits` branch answers it: its own namespace's app. */
|
|
33
34
|
export async function readableOrDeleted(db, ev, alias) {
|
|
34
35
|
const live = await permits(db, ev, 'read', alias);
|
|
35
|
-
const ceiling = await resolveCeiling(db, ev.reader, 'read');
|
|
36
|
+
const ceiling = await resolveCeiling(db, ev.reader, ev.userId, 'read');
|
|
36
37
|
const grants = compilePermission(await loadDefinitions(db), ev.subjects, OBJECT_TYPE, 'read', raw('gr.id'));
|
|
37
38
|
const chain = { sql: `EXISTS (WITH RECURSIVE up(id, depth) AS (
|
|
38
39
|
SELECT ${alias}.id, 0
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import { type QuarantinedPlugin } from '../plugin-quarantine.js';
|
|
2
1
|
import { type PackageFile } from './package-files.js';
|
|
3
2
|
import type { ReposFn } from './reader.js';
|
|
4
3
|
import type { ObjectRow } from './types.js';
|
|
4
|
+
import type { QuarantinedPlugin } from '../plugin-quarantine.js';
|
|
5
5
|
import type { CanvasDb } from '../canvas/db.js';
|
|
6
|
-
export interface PackageSource {
|
|
7
|
-
plugin: string;
|
|
8
|
-
dir: string;
|
|
9
|
-
}
|
|
10
|
-
/** The builtin corpus (app `crtr`), then every enabled user-scope plugin's `memory/` folder. */
|
|
11
|
-
export declare function packageSources(): PackageSource[];
|
|
12
6
|
export interface PackageReconcileReport {
|
|
13
7
|
created: number;
|
|
14
8
|
updated: number;
|
|
@@ -25,17 +19,14 @@ export interface PackageReconcileReport {
|
|
|
25
19
|
/** plugin directories with an unreadable manifest or command file, moved aside before reconciling */
|
|
26
20
|
quarantined: QuarantinedPlugin[];
|
|
27
21
|
}
|
|
28
|
-
/** Converge every package's documents onto its files. Run inside the caller's transaction.
|
|
29
|
-
* Idempotent: a second run over unchanged files changes no document, revision or rule (it re-derives
|
|
30
|
-
* each document's link edges, which land the same). A changed body replaces the
|
|
31
|
-
* document's single revision and publishes `edited`; a vanished file (or plugin) marks its
|
|
32
|
-
* document deleted and publishes `deleted`. */
|
|
33
|
-
export declare function reconcilePackageDocuments(db: CanvasDb, explicitSources?: readonly PackageSource[]): Promise<PackageReconcileReport>;
|
|
34
22
|
/** Record a package document's `links-to` edges the way a runtime document gets them, so delivery
|
|
35
|
-
* attaches the link previews. Each `[[name]]`
|
|
36
|
-
*
|
|
23
|
+
* attaches the link previews. Each `[[name]]` links only when it names a shipped document of any
|
|
24
|
+
* published package (builtin, provider or plugin): a package never links to a user's or a
|
|
37
25
|
* runtime document that happens to share a bare name. A name that resolves to nothing stays text.
|
|
38
26
|
* Edges are rewritten only when they differ, so an unchanged pass writes nothing. */
|
|
39
27
|
export declare function deriveLinks(db: CanvasDb, owner: ObjectRow, doc: ObjectRow, body: string): Promise<void>;
|
|
28
|
+
/** Re-derive the links of every live shipped document of every app. A package published before the one its documents name has no edge to it yet;
|
|
29
|
+
* publishing the missing one calls this, so the edges do not depend on which package arrived first. */
|
|
30
|
+
export declare function relinkAllShipped(db: CanvasDb): Promise<void>;
|
|
40
31
|
export declare function convergeFile(db: CanvasDb, owner: ObjectRow, file: PackageFile, report: PackageReconcileReport, reposFn: ReposFn): Promise<string>;
|
|
41
32
|
export declare function deleteMissing(db: CanvasDb, owner: ObjectRow, keep: ReadonlySet<string>, report: PackageReconcileReport, reposFn: ReposFn): Promise<void>;
|