@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
|
@@ -10,12 +10,33 @@ export interface PersonSnapshot {
|
|
|
10
10
|
defaults: Record<string, string>;
|
|
11
11
|
settings: Record<string, Record<string, unknown>>;
|
|
12
12
|
state: Record<string, ScopeState>;
|
|
13
|
+
/** App installs (`app:<id>` → plugins, marketplaces) with a revision covering only that app's block. Cloud roles write plugins here, never to `settings`. */
|
|
14
|
+
apps: Record<string, AppInstallBlock>;
|
|
15
|
+
}
|
|
16
|
+
export interface AppInstallBlock {
|
|
17
|
+
plugins: Record<string, {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
source_marketplace?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
}>;
|
|
22
|
+
marketplaces: Record<string, {
|
|
23
|
+
url?: string;
|
|
24
|
+
ref?: string;
|
|
25
|
+
installed_at?: string;
|
|
26
|
+
}>;
|
|
27
|
+
revision: string;
|
|
13
28
|
}
|
|
14
29
|
export type SettingsOp = {
|
|
15
30
|
op: 'settings.replace';
|
|
16
31
|
owner: string;
|
|
17
32
|
raw: Record<string, unknown>;
|
|
18
33
|
ifRevision: string;
|
|
34
|
+
} | {
|
|
35
|
+
op: 'app-plugins.replace';
|
|
36
|
+
app: string;
|
|
37
|
+
plugins: AppInstallBlock['plugins'];
|
|
38
|
+
marketplaces: AppInstallBlock['marketplaces'];
|
|
39
|
+
ifRevision: string;
|
|
19
40
|
} | {
|
|
20
41
|
op: 'state.write';
|
|
21
42
|
owner: string;
|
|
@@ -40,6 +61,8 @@ export declare class PersonSettingsNotLoadedError extends Error {
|
|
|
40
61
|
constructor(person: string);
|
|
41
62
|
}
|
|
42
63
|
declare const BLOCKS: readonly ["plugins", "marketplaces"];
|
|
64
|
+
/** The revision of an app's install block; `absent` for an app with no rows. */
|
|
65
|
+
export declare function appRevisionOf(block: Pick<AppInstallBlock, 'plugins' | 'marketplaces'> | undefined): string;
|
|
43
66
|
export declare function revisionOf(raw: Record<string, unknown> | undefined): string;
|
|
44
67
|
export declare function installPersonSnapshot(person: string, snapshot: PersonSnapshot): void;
|
|
45
68
|
export declare function heldPersons(): string[];
|
|
@@ -13,6 +13,10 @@ export class PersonSettingsNotLoadedError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
const BLOCKS = ['plugins', 'marketplaces'];
|
|
15
15
|
const caches = new Map();
|
|
16
|
+
/** The revision of an app's install block; `absent` for an app with no rows. */
|
|
17
|
+
export function appRevisionOf(block) {
|
|
18
|
+
return block === undefined ? 'absent' : revisionOf({ plugins: block.plugins, marketplaces: block.marketplaces });
|
|
19
|
+
}
|
|
16
20
|
export function revisionOf(raw) {
|
|
17
21
|
return raw === undefined ? 'absent' : createHash('sha256').update(JSON.stringify(raw)).digest('hex');
|
|
18
22
|
}
|
|
@@ -20,7 +24,7 @@ export function revisionOf(raw) {
|
|
|
20
24
|
export function installPersonSnapshot(person, snapshot) { caches.set(person, snapshot); }
|
|
21
25
|
export function heldPersons() { return [...caches.keys()]; }
|
|
22
26
|
export function dropPersonSnapshots() { caches.clear(); }
|
|
23
|
-
const emptySnapshot = () => ({ profiles: [], defaults: {}, settings: {}, state: {} });
|
|
27
|
+
const emptySnapshot = () => ({ profiles: [], defaults: {}, settings: {}, state: {}, apps: {} });
|
|
24
28
|
let prelude;
|
|
25
29
|
/** Before the install's person is resolved (a Mac's first boot reads its config to resolve it), reads answer from this snapshot of
|
|
26
30
|
* the old local store; `loadPersonSettings` replaces it once the person is known. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.590",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api/index.js",
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { CanvasDb } from '../canvas/db.js';
|
|
2
|
-
export interface KindsReport {
|
|
3
|
-
/** every kind a valid plugin declares that now has its row, and what upserting it did */
|
|
4
|
-
applied: Array<{
|
|
5
|
-
plugin: string;
|
|
6
|
-
kind: string;
|
|
7
|
-
outcome: 'inserted' | 'updated' | 'unchanged';
|
|
8
|
-
}>;
|
|
9
|
-
/** kinds another owner holds; the plugin's other kinds still applied */
|
|
10
|
-
refused: Array<{
|
|
11
|
-
plugin: string;
|
|
12
|
-
kind: string;
|
|
13
|
-
code: 'kind_owned';
|
|
14
|
-
message: string;
|
|
15
|
-
}>;
|
|
16
|
-
/** enabled plugins whose manifest or commands are invalid (no kind update, no token); a provider bundle's kinds reconcile without a token, and plugins whose app handle another owner holds */
|
|
17
|
-
invalid: Array<{
|
|
18
|
-
plugin: string;
|
|
19
|
-
error: string;
|
|
20
|
-
}>;
|
|
21
|
-
}
|
|
22
|
-
export declare function reconcilePackageKinds(db: CanvasDb): Promise<KindsReport>;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/** Installed plugins' `object_kinds` blocks → `object_kinds` rows. Runs in the caller's write transaction.
|
|
2
|
-
* A kind is never deleted and its catalog is never altered when undeclared: only `declared` flips, so the
|
|
3
|
-
* instances of a dropped kind go inert instead of vanishing. */
|
|
4
|
-
import { CrtrError } from '../errors.js';
|
|
5
|
-
import { invalidAppPlugins, kindPlugins } from '../plugin-app.js';
|
|
6
|
-
import { upsertKind } from './custom.js';
|
|
7
|
-
import { ensureOwner } from './objects.js';
|
|
8
|
-
export async function reconcilePackageKinds(db) {
|
|
9
|
-
const report = { applied: [], refused: [], invalid: invalidAppPlugins().map((i) => ({ plugin: i.plugin.name, error: i.error })) };
|
|
10
|
-
const declared = new Set();
|
|
11
|
-
for (const plugin of kindPlugins()) {
|
|
12
|
-
const kinds = Object.entries(plugin.manifest.object_kinds ?? {});
|
|
13
|
-
if (kinds.length === 0)
|
|
14
|
-
continue;
|
|
15
|
-
let owner;
|
|
16
|
-
try {
|
|
17
|
-
owner = await ensureOwner(db, 'app', 'app', `app:${plugin.name}`, plugin.name);
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
if (!(error instanceof CrtrError) || error.code !== 'conflict')
|
|
21
|
-
throw error;
|
|
22
|
-
report.invalid.push({ plugin: plugin.name, error: error.message });
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
for (const [kind, decl] of kinds) {
|
|
26
|
-
try {
|
|
27
|
-
const result = await upsertKind(db, {
|
|
28
|
-
name: kind, owner, description: decl.description, events: decl.events,
|
|
29
|
-
read: decl.read === undefined ? null : { command: decl.read.command }, source: 'manifest',
|
|
30
|
-
});
|
|
31
|
-
if (result.outcome === 'skipped')
|
|
32
|
-
continue;
|
|
33
|
-
declared.add(kind);
|
|
34
|
-
report.applied.push({ plugin: plugin.name, kind, outcome: result.outcome });
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (!(error instanceof CrtrError) || error.code !== 'kind_owned')
|
|
38
|
-
throw error;
|
|
39
|
-
report.refused.push({ plugin: plugin.name, kind, code: 'kind_owned', message: error.message });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const rows = await db.prepare("SELECT name FROM object_kinds WHERE source = 'manifest' AND declared = 1").all();
|
|
44
|
-
for (const { name } of rows) {
|
|
45
|
-
if (!declared.has(name))
|
|
46
|
-
await db.prepare('UPDATE object_kinds SET declared = 0 WHERE name = ?').run(name);
|
|
47
|
-
}
|
|
48
|
-
return report;
|
|
49
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/** One enabled plugin row of the person's user scope, with its marketplace's url and ref. */
|
|
2
|
-
export interface PluginInstall {
|
|
3
|
-
name: string;
|
|
4
|
-
version: string | null;
|
|
5
|
-
sourcePath: string | null;
|
|
6
|
-
marketplace: {
|
|
7
|
-
name: string;
|
|
8
|
-
url: string | null;
|
|
9
|
-
ref: string | null;
|
|
10
|
-
} | null;
|
|
11
|
-
/** A directory provider bundle (`isProviderBundleRow`): a provider call installs it into this process's own plugin directory, so there is nothing to fetch. */
|
|
12
|
-
providerBundle: boolean;
|
|
13
|
-
}
|
|
14
|
-
/** The directory holding this person's materialized plugins as `<dir>/<plugin name>`; undefined when none were materialized. */
|
|
15
|
-
export declare function materializedPluginsDir(person: string): string | undefined;
|
|
16
|
-
/** Install `installs` for `person` at `revision` into this process's plugin cache, once per (person, revision). When a plugin cannot
|
|
17
|
-
* be installed — a source on another machine, a marketplace that does not list it, a failed clone — the failure is held for that person
|
|
18
|
-
* and thrown by the next read of their plugins (`materializedPluginsDir`), so a request that never reads plugins, such as the one that
|
|
19
|
-
* fixes the settings, still works; the next load retries. */
|
|
20
|
-
export declare function materializePlugins(person: string, revision: number, installs: readonly PluginInstall[]): Promise<void>;
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// materialized-plugins.ts — the plugin bytes a cloud-role process holds for the people it serves (D84). A person's enabled plugins are
|
|
2
|
-
// rows (`scope_plugins`, `scope_marketplaces`); a fresh API process has no plugin directories, so before it reads plugin manifests it
|
|
3
|
-
// installs each enabled plugin from its marketplace into a process-local directory keyed by (person, plugins_revision). Leaf-safe:
|
|
4
|
-
// the installed-plugin reader imports this, so it imports no database. `loadPersonSettings` (person-rows.ts) fills it.
|
|
5
|
-
import { cpSync, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
8
|
-
import { CrtrError } from '../errors.js';
|
|
9
|
-
import { gitAsync } from '../git.js';
|
|
10
|
-
import { readMarketplaceManifest } from '../manifest.js';
|
|
11
|
-
const materialized = new Map();
|
|
12
|
-
const inFlight = new Map();
|
|
13
|
-
/** The directory holding this person's materialized plugins as `<dir>/<plugin name>`; undefined when none were materialized. */
|
|
14
|
-
export function materializedPluginsDir(person) {
|
|
15
|
-
const held = materialized.get(person);
|
|
16
|
-
if (held !== undefined && 'failure' in held)
|
|
17
|
-
throw held.failure;
|
|
18
|
-
return held?.dir;
|
|
19
|
-
}
|
|
20
|
-
function cacheRoot(person) { return join(tmpdir(), 'crtr-plugin-cache', person.replace(/[^A-Za-z0-9._-]/g, '_')); }
|
|
21
|
-
async function cloneInto(url, ref, dest, what) {
|
|
22
|
-
const res = await gitAsync(['clone', '--depth', '1', ...(ref ? ['--branch', ref] : []), url, dest]);
|
|
23
|
-
if (res.status !== 0)
|
|
24
|
-
throw new CrtrError('plugin_install_failed', `${what}: git clone ${url} failed: ${res.stderr.trim()}`);
|
|
25
|
-
}
|
|
26
|
-
async function installAll(staging, installs) {
|
|
27
|
-
const marketRoots = new Map();
|
|
28
|
-
mkdirSync(join(staging, 'plugins'), { recursive: true });
|
|
29
|
-
for (const plugin of installs) {
|
|
30
|
-
if (plugin.sourcePath !== null)
|
|
31
|
-
throw new CrtrError('plugin_source_unreachable', `${plugin.name}: its source is ${plugin.sourcePath} on the machine that registered it, which the control plane cannot read`);
|
|
32
|
-
const market = plugin.marketplace;
|
|
33
|
-
if (plugin.providerBundle)
|
|
34
|
-
continue;
|
|
35
|
-
if (market === null)
|
|
36
|
-
throw new CrtrError('plugin_source_unreachable', `${plugin.name}: it names neither a marketplace nor a path`);
|
|
37
|
-
if (market.url === null || market.url === '')
|
|
38
|
-
throw new CrtrError('plugin_source_unreachable', `${plugin.name}: its marketplace ${market.name} has no url`);
|
|
39
|
-
const key = `${market.url}\0${market.ref ?? ''}`;
|
|
40
|
-
let root = marketRoots.get(key);
|
|
41
|
-
if (root === undefined) {
|
|
42
|
-
root = join(staging, 'marketplaces', String(marketRoots.size));
|
|
43
|
-
await cloneInto(market.url, market.ref, root, `${plugin.name}: marketplace ${market.name}`);
|
|
44
|
-
marketRoots.set(key, root);
|
|
45
|
-
}
|
|
46
|
-
const entry = readMarketplaceManifest(root)?.plugins.find((p) => p.name === plugin.name);
|
|
47
|
-
if (entry === undefined)
|
|
48
|
-
throw new CrtrError('plugin_install_failed', `${plugin.name}: marketplace ${market.name} (${market.url}) does not list it`);
|
|
49
|
-
const dest = join(staging, 'plugins', plugin.name);
|
|
50
|
-
if (isAbsolute(entry.source) || /^[a-z][a-z0-9+.-]*:|^git@/i.test(entry.source) || entry.source.endsWith('.git')) {
|
|
51
|
-
await cloneInto(entry.source, null, dest, plugin.name);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const from = resolve(root, entry.source);
|
|
55
|
-
const inside = relative(root, from);
|
|
56
|
-
if (inside.startsWith('..') || isAbsolute(inside))
|
|
57
|
-
throw new CrtrError('plugin_install_failed', `${plugin.name}: its source ${entry.source} leaves the marketplace`);
|
|
58
|
-
if (!existsSync(from))
|
|
59
|
-
throw new CrtrError('plugin_install_failed', `${plugin.name}: its source ${entry.source} is not in marketplace ${market.name}`);
|
|
60
|
-
cpSync(from, dest, { recursive: true });
|
|
61
|
-
}
|
|
62
|
-
rmSync(join(staging, 'marketplaces'), { recursive: true, force: true });
|
|
63
|
-
}
|
|
64
|
-
/** Install `installs` for `person` at `revision` into this process's plugin cache, once per (person, revision). When a plugin cannot
|
|
65
|
-
* be installed — a source on another machine, a marketplace that does not list it, a failed clone — the failure is held for that person
|
|
66
|
-
* and thrown by the next read of their plugins (`materializedPluginsDir`), so a request that never reads plugins, such as the one that
|
|
67
|
-
* fixes the settings, still works; the next load retries. */
|
|
68
|
-
export async function materializePlugins(person, revision, installs) {
|
|
69
|
-
const held = materialized.get(person);
|
|
70
|
-
if (held?.revision === revision && !('failure' in held))
|
|
71
|
-
return;
|
|
72
|
-
const final = join(cacheRoot(person), String(revision));
|
|
73
|
-
const key = `${person}\0${revision}`;
|
|
74
|
-
let pending = inFlight.get(key);
|
|
75
|
-
if (pending === undefined) {
|
|
76
|
-
pending = (async () => {
|
|
77
|
-
if (!existsSync(join(final, 'plugins'))) {
|
|
78
|
-
const staging = join(cacheRoot(person), `${revision}.${process.pid}.${Date.now()}`);
|
|
79
|
-
try {
|
|
80
|
-
await installAll(staging, installs);
|
|
81
|
-
try {
|
|
82
|
-
renameSync(staging, final);
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
if (!existsSync(join(final, 'plugins')))
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
rmSync(staging, { recursive: true, force: true });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return join(final, 'plugins');
|
|
94
|
-
})().finally(() => inFlight.delete(key));
|
|
95
|
-
inFlight.set(key, pending);
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
materialized.set(person, { revision, dir: await pending });
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (!(error instanceof CrtrError))
|
|
102
|
-
throw error;
|
|
103
|
-
materialized.set(person, { revision, failure: error });
|
|
104
|
-
}
|
|
105
|
-
}
|