@north-light/api 0.3.592 → 0.3.593

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.
@@ -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.44";
2
+ export declare const CONTRACT_LEVEL = "1.45";
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`. */
@@ -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.44';
15
+ export const CONTRACT_LEVEL = '1.45';
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`. */
@@ -61,6 +61,12 @@ export interface ImportRunDTO {
61
61
  verify: ImportVerifyDTO | null;
62
62
  failure: ImportFailureDTO | null;
63
63
  unresolved_plugins: string[];
64
+ /** Plugins whose marketplace source the directory would not publish: the install is imported, its documents become the person's own copies. `holder` names the source that holds a `name_taken` name. */
65
+ refused_plugins?: Array<{
66
+ plugin: string;
67
+ reason: 'name_taken' | 'not_public' | 'not_found';
68
+ holder?: string;
69
+ }>;
64
70
  created: string;
65
71
  loaded: string | null;
66
72
  finished: string | null;
@@ -3,7 +3,7 @@ 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';
6
+ import { installsLiveInAppBlock } from '../../shared/env.js';
7
7
  import { listAppMarketplaces } from './app-installs.js';
8
8
  export const marketList = defineLeaf({
9
9
  name: 'list',
@@ -26,7 +26,7 @@ export const marketList = defineLeaf({
26
26
  effects: ['None. Read-only.'],
27
27
  },
28
28
  run: async (input) => {
29
- if (isSandboxedNode())
29
+ if (installsLiveInAppBlock())
30
30
  return listAppMarketplaces();
31
31
  const scopeInput = input['scope'];
32
32
  const limitRaw = input['limit'];
@@ -4,7 +4,7 @@ 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';
7
+ import { installsLiveInAppBlock } from '../../shared/env.js';
8
8
  import { addAppMarketplace, removeAppMarketplace } from './app-installs.js';
9
9
  import { updateConfig, updateState, ensureScopeInitialized, readConfig } from '../../core/config.js';
10
10
  import { pathExists, ensureDir, removePath, isSymlink, nowIso, readSymlinkTarget } from '../../core/fs-utils.js';
@@ -41,13 +41,20 @@ export async function addMarketplace(opts) {
41
41
  renameSync(tempDest, finalDest);
42
42
  }
43
43
  const effectiveRef = ref !== undefined ? ref : 'main';
44
- await updateConfig(scope, (cfg) => {
45
- cfg.marketplaces[finalName] = {
46
- url,
47
- ref: effectiveRef,
48
- installed_at: nowIso(),
49
- };
50
- });
44
+ // A refused registration leaves no clone behind: a directory without its config row would list as a marketplace with no url.
45
+ try {
46
+ await updateConfig(scope, (cfg) => {
47
+ cfg.marketplaces[finalName] = {
48
+ url,
49
+ ref: effectiveRef,
50
+ installed_at: nowIso(),
51
+ };
52
+ });
53
+ }
54
+ catch (error) {
55
+ removePath(finalDest);
56
+ throw error;
57
+ }
51
58
  return { name: finalName, scope, path: finalDest };
52
59
  }
53
60
  export const marketAdd = defineLeaf({
@@ -74,7 +81,7 @@ export const marketAdd = defineLeaf({
74
81
  const url = input['url'];
75
82
  const ref = input['ref'];
76
83
  const scopeInput = input['scope'];
77
- if (isSandboxedNode())
84
+ if (installsLiveInAppBlock())
78
85
  return await addAppMarketplace(url, ref);
79
86
  return addMarketplace({ url, ref, scope: resolveInstallScope(scopeInput) });
80
87
  },
@@ -96,7 +103,7 @@ export const marketRemove = defineLeaf({
96
103
  },
97
104
  run: async (input) => {
98
105
  const name = input['name'];
99
- if (isSandboxedNode())
106
+ if (installsLiveInAppBlock())
100
107
  return await removeAppMarketplace(name);
101
108
  const scopeInput = input['scope'];
102
109
  let targetScope;
@@ -1,6 +1,6 @@
1
1
  import { defineLeaf } from '../../core/command.js';
2
2
  import { notFound } from '../../core/errors.js';
3
- import { isSandboxedNode } from '../../shared/env.js';
3
+ import { installsLiveInAppBlock } from '../../shared/env.js';
4
4
  import { listAppPlugins } from './app-installs.js';
5
5
  import { paginate } from '../../core/pagination.js';
6
6
  import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
@@ -46,7 +46,7 @@ export const pluginList = defineLeaf({
46
46
  run: async (input) => {
47
47
  const scopeInput = input['scope'];
48
48
  const includeDisabled = input['includeDisabled'] ?? false;
49
- if (isSandboxedNode())
49
+ if (installsLiveInAppBlock())
50
50
  return await listAppPlugins(includeDisabled);
51
51
  const limitRaw = input['limit'];
52
52
  const limit = limitRaw !== undefined ? Math.min(Math.max(1, limitRaw), 200) : 50;
@@ -5,7 +5,7 @@ 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';
8
+ import { installsLiveInAppBlock } from '../../shared/env.js';
9
9
  import { installAppPlugin, removeAppPlugin, setAppPluginEnabled, updateAppPlugins } from './app-installs.js';
10
10
  import { notFound, usage, general, network, CrtrError } from '../../core/errors.js';
11
11
  import { findMarketplaceByName, findPluginByName, listAllPlugins } from '../../core/resolver.js';
@@ -812,7 +812,7 @@ export const pluginInstall = defineLeaf({
812
812
  const authEnv = input['authEnv'];
813
813
  const scopeInput = input['scope'];
814
814
  const gitRef = input['ref'];
815
- if (isSandboxedNode()) {
815
+ if (installsLiveInAppBlock()) {
816
816
  if (rawRef === undefined || endpoint !== undefined || name !== undefined || authEnv !== undefined || gitRef !== undefined || scopeInput !== undefined)
817
817
  throw usage('a sandboxed node installs only INSTALL-REF as marketplace/plugin');
818
818
  return await installAppPlugin(rawRef);
@@ -874,7 +874,7 @@ export const pluginRemove = defineLeaf({
874
874
  if (name === 'crtr') {
875
875
  throw usage('cannot remove builtin plugin "crtr" — it ships with the binary');
876
876
  }
877
- if (isSandboxedNode())
877
+ if (installsLiveInAppBlock())
878
878
  return await removeAppPlugin(name);
879
879
  let scopes;
880
880
  if (scopeInput !== undefined) {
@@ -939,7 +939,7 @@ export const pluginEnable = defineLeaf({
939
939
  },
940
940
  run: async (input) => {
941
941
  const name = input['name'];
942
- if (isSandboxedNode())
942
+ if (installsLiveInAppBlock())
943
943
  return await setAppPluginEnabled(name, true);
944
944
  const target = resolvePluginConfigTarget(name, input['scope']);
945
945
  if (target === null)
@@ -972,7 +972,7 @@ export const pluginDisable = defineLeaf({
972
972
  effects: ['Sets plugin enabled=false in config.json.'],
973
973
  },
974
974
  run: async (input) => {
975
- if (isSandboxedNode())
975
+ if (installsLiveInAppBlock())
976
976
  return await setAppPluginEnabled(input['name'], false);
977
977
  const result = await setPluginEnabled(input, false);
978
978
  await reconcilePluginDocuments();
@@ -1002,7 +1002,7 @@ export const pluginUpdate = defineLeaf({
1002
1002
  },
1003
1003
  run: async (input) => {
1004
1004
  const name = input['name'];
1005
- if (isSandboxedNode())
1005
+ if (installsLiveInAppBlock())
1006
1006
  return await updateAppPlugins(name);
1007
1007
  const scopeInput = input['scope'];
1008
1008
  const scope = scopeInput === undefined ? undefined : resolveScopeArg(scopeInput);
@@ -16,6 +16,10 @@ export declare function isSandboxedNode(): boolean;
16
16
  * descriptions (`GET /v1/providers/tools` through that relay); the relay's node token stays the authority. It is not `isSandboxedNode()`,
17
17
  * which gates owner-only commands and launch-snapshot reads on a Linux launcher mount. */
18
18
  export declare function providerProjectionOverBrokerRelay(): boolean;
19
+ /** True when this CLI's plugin and marketplace installs are its app's install block on the control plane (`app-plugins.replace` over `/v1`), not
20
+ * directories in a scope: a Linux launcher sandbox (`isSandboxedNode()`) or a fleet node reaching `/v1` through its broker relay
21
+ * (`providerProjectionOverBrokerRelay()`, which has no launcher mount). The `pkg plugin` and `pkg market` leaves choose their path with it. */
22
+ export declare function installsLiveInAppBlock(): boolean;
19
23
  /** The app that owns the current node, supplied in its broker launch identity. */
20
24
  export declare function envGrantee(): string | undefined;
21
25
  /** Set to '1' by `nodeEnv` for an app-started run's brokers, which admit inbox
@@ -71,6 +71,12 @@ export function isSandboxedNode() {
71
71
  export function providerProjectionOverBrokerRelay() {
72
72
  return /^https?:\/\//.test(process.env['CRTR_API_URL'] ?? '') && (process.env['CRTR_API_SOCKET'] ?? '') !== '';
73
73
  }
74
+ /** True when this CLI's plugin and marketplace installs are its app's install block on the control plane (`app-plugins.replace` over `/v1`), not
75
+ * directories in a scope: a Linux launcher sandbox (`isSandboxedNode()`) or a fleet node reaching `/v1` through its broker relay
76
+ * (`providerProjectionOverBrokerRelay()`, which has no launcher mount). The `pkg plugin` and `pkg market` leaves choose their path with it. */
77
+ export function installsLiveInAppBlock() {
78
+ return isSandboxedNode() || providerProjectionOverBrokerRelay();
79
+ }
74
80
  /** The app that owns the current node, supplied in its broker launch identity. */
75
81
  export function envGrantee() {
76
82
  return process.env['CRTR_GRANTEE'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.592",
3
+ "version": "0.3.593",
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",