@objectstack/cli 7.4.1 → 7.6.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/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +22 -0
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/dev.d.ts +0 -10
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +30 -77
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/plugin/build.d.ts +15 -0
- package/dist/commands/plugin/build.d.ts.map +1 -0
- package/dist/commands/plugin/build.js +199 -0
- package/dist/commands/plugin/build.js.map +1 -0
- package/dist/commands/plugin/publish.d.ts +26 -0
- package/dist/commands/plugin/publish.d.ts.map +1 -0
- package/dist/commands/plugin/publish.js +211 -0
- package/dist/commands/plugin/publish.js.map +1 -0
- package/dist/commands/plugin/sign.d.ts +15 -0
- package/dist/commands/plugin/sign.d.ts.map +1 -0
- package/dist/commands/plugin/sign.js +102 -0
- package/dist/commands/plugin/sign.js.map +1 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +175 -23
- package/dist/commands/serve.js.map +1 -1
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +6 -0
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/osplugin.d.ts +44 -0
- package/dist/utils/osplugin.d.ts.map +1 -0
- package/dist/utils/osplugin.js +151 -0
- package/dist/utils/osplugin.js.map +1 -0
- package/dist/utils/validate-expressions.d.ts +13 -0
- package/dist/utils/validate-expressions.d.ts.map +1 -0
- package/dist/utils/validate-expressions.js +101 -0
- package/dist/utils/validate-expressions.js.map +1 -0
- package/dist/utils/validate-expressions.test.d.ts +2 -0
- package/dist/utils/validate-expressions.test.d.ts.map +1 -0
- package/dist/utils/validate-expressions.test.js +65 -0
- package/dist/utils/validate-expressions.test.js.map +1 -0
- package/package.json +48 -50
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* `os plugin publish` — upload a signed `.osplugin` to ObjectStack Cloud
|
|
4
|
+
* (ADR-0025 §3.4 step 3). Completes the build → sign → publish pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Read the `.osplugin` bytes + the detached `.sig` (publisher signature).
|
|
8
|
+
* 2. Extract the compiled `objectstack.plugin.json` from inside the
|
|
9
|
+
* artifact (id / version / name / runtime / permissions / integrity).
|
|
10
|
+
* 3. POST /cloud/packages — ensure the sys_package row exists.
|
|
11
|
+
* 4. POST /cloud/packages/:id/versions with `artifact_kind: 'plugin'`,
|
|
12
|
+
* the base64 artifact, the declared manifest, the signature, and the
|
|
13
|
+
* whole-artifact sha256 checksum. The cloud verifies the signature,
|
|
14
|
+
* audits permissions/runtime tier, stores the blob, and sets
|
|
15
|
+
* listing_status=pending_review (or approved with --auto-approve).
|
|
16
|
+
*
|
|
17
|
+
* The platform counter-signature is written by the marketplace review/approve
|
|
18
|
+
* flow, not here.
|
|
19
|
+
*/
|
|
20
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { resolve as resolvePath, basename } from 'node:path';
|
|
23
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
24
|
+
import { printHeader, printKV, printSuccess, printError, printStep } from '../../utils/format.js';
|
|
25
|
+
import { DEFAULT_CLOUD_URL, tryReadCloudConfig } from '../../utils/cloud-config.js';
|
|
26
|
+
import { OSPLUGIN_EXT, sha256Hex, readOspluginManifest } from '../../utils/osplugin.js';
|
|
27
|
+
export default class PluginPublish extends Command {
|
|
28
|
+
static description = 'Publish a signed .osplugin to ObjectStack Cloud (ADR-0025 §3.4)';
|
|
29
|
+
static examples = [
|
|
30
|
+
'$ os plugin publish ./com.acme.stripe-1.0.0.osplugin --visibility marketplace --submit',
|
|
31
|
+
'$ os plugin publish ./x.osplugin --sig ./x.osplugin.sig --org org_123',
|
|
32
|
+
'$ OS_CLOUD_URL=http://localhost:4000 os plugin publish ./x.osplugin --auto-approve',
|
|
33
|
+
];
|
|
34
|
+
static args = {
|
|
35
|
+
artifact: Args.string({ description: 'Path to the .osplugin (default: the single .osplugin in cwd)', required: false }),
|
|
36
|
+
};
|
|
37
|
+
static flags = {
|
|
38
|
+
server: Flags.string({ char: 's', description: 'Cloud control-plane URL', env: 'OS_CLOUD_URL', default: DEFAULT_CLOUD_URL }),
|
|
39
|
+
token: Flags.string({ char: 't', description: 'Cloud API key (bearer)', env: 'OS_CLOUD_API_KEY' }),
|
|
40
|
+
sig: Flags.string({ description: 'Path to the detached signature (default: <artifact>.sig)' }),
|
|
41
|
+
'manifest-id': Flags.string({ description: 'Override package id (default: from the manifest)' }),
|
|
42
|
+
'display-name': Flags.string({ description: 'Marketplace display name (default: manifest.name)' }),
|
|
43
|
+
visibility: Flags.string({ description: 'Who can see/install', options: ['private', 'org', 'marketplace'], default: 'private' }),
|
|
44
|
+
org: Flags.string({ description: 'owner_org_id (service mode)', env: 'OS_ORG_ID' }),
|
|
45
|
+
note: Flags.string({ char: 'n', description: 'Release notes (markdown ok)' }),
|
|
46
|
+
'pre-release': Flags.boolean({ description: 'Mark as a pre-release', default: false }),
|
|
47
|
+
submit: Flags.boolean({ description: 'Submit for marketplace review after publish', default: false }),
|
|
48
|
+
'auto-approve': Flags.boolean({ description: 'Platform admin only: skip review queue', default: false }),
|
|
49
|
+
timeout: Flags.integer({ description: 'HTTP timeout (ms, 0 disables)', env: 'OS_CLOUD_TIMEOUT_MS', default: 120_000 }),
|
|
50
|
+
};
|
|
51
|
+
async run() {
|
|
52
|
+
const { args, flags } = await this.parse(PluginPublish);
|
|
53
|
+
printHeader('Publish Plugin');
|
|
54
|
+
// 1. Resolve the artifact path. ────────────────────────────────────
|
|
55
|
+
let artifactPath;
|
|
56
|
+
if (args.artifact) {
|
|
57
|
+
artifactPath = resolvePath(process.cwd(), args.artifact);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const here = (await readdir(process.cwd())).filter((f) => f.endsWith(OSPLUGIN_EXT));
|
|
61
|
+
if (here.length === 0) {
|
|
62
|
+
printError(`No ${OSPLUGIN_EXT} found in cwd. Run \`os plugin build\` first, or pass a path.`);
|
|
63
|
+
this.exit(1);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (here.length > 1) {
|
|
67
|
+
printError(`Multiple ${OSPLUGIN_EXT} files found — pass one explicitly.`);
|
|
68
|
+
this.exit(1);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
artifactPath = resolvePath(process.cwd(), here[0]);
|
|
72
|
+
}
|
|
73
|
+
if (!existsSync(artifactPath)) {
|
|
74
|
+
printError(`Artifact not found: ${artifactPath}`);
|
|
75
|
+
this.exit(1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const bytes = new Uint8Array(await readFile(artifactPath));
|
|
79
|
+
const checksum = sha256Hex(bytes);
|
|
80
|
+
const base64 = Buffer.from(bytes).toString('base64');
|
|
81
|
+
// 2. Extract the compiled manifest from inside the artifact. ────────
|
|
82
|
+
let manifest;
|
|
83
|
+
try {
|
|
84
|
+
manifest = readOspluginManifest(bytes);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
printError(`Cannot read manifest from artifact: ${err?.message ?? err}`);
|
|
88
|
+
this.exit(1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const id = String(flags['manifest-id'] ?? manifest.id ?? '').trim();
|
|
92
|
+
const version = String(manifest.version ?? '').trim();
|
|
93
|
+
const displayName = String(flags['display-name'] ?? manifest.name ?? id).trim();
|
|
94
|
+
if (!id || !version) {
|
|
95
|
+
printError('Artifact manifest is missing id or version.');
|
|
96
|
+
this.exit(1);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
printStep(`${id}@${version} (${(bytes.byteLength / 1024).toFixed(1)} KB, runtime: ${manifest.runtime ?? 'unset'})`);
|
|
100
|
+
// 3. Detached publisher signature. ─────────────────────────────────
|
|
101
|
+
const sigPath = resolvePath(process.cwd(), flags.sig ?? `${artifactPath}.sig`);
|
|
102
|
+
let signature;
|
|
103
|
+
if (existsSync(sigPath)) {
|
|
104
|
+
signature = (await readFile(sigPath, 'utf-8')).trim();
|
|
105
|
+
printKV(' Signature', signature.length > 48 ? signature.slice(0, 48) + '…' : signature);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
printStep(`No signature sidecar at ${basename(sigPath)} — publishing UNSIGNED (a "node"-tier plugin will be rejected by the server unless the publisher is verified). Run \`os plugin sign\` first.`);
|
|
109
|
+
}
|
|
110
|
+
// 4. Auth + server URL (same precedence as `os package publish`). ───
|
|
111
|
+
let token = flags.token ?? process.env.OS_TOKEN ?? undefined;
|
|
112
|
+
let baseUrl = flags.server.replace(/\/+$/, '');
|
|
113
|
+
const serverFlagWasDefault = !process.env.OS_CLOUD_URL && baseUrl === DEFAULT_CLOUD_URL;
|
|
114
|
+
if (!token || serverFlagWasDefault) {
|
|
115
|
+
const stored = await tryReadCloudConfig();
|
|
116
|
+
if (!token && stored?.token)
|
|
117
|
+
token = stored.token;
|
|
118
|
+
if (serverFlagWasDefault && stored?.url)
|
|
119
|
+
baseUrl = stored.url.replace(/\/+$/, '');
|
|
120
|
+
}
|
|
121
|
+
if (!token) {
|
|
122
|
+
printError('Not logged in. Run `os cloud login`, or pass --token / set $OS_CLOUD_API_KEY.');
|
|
123
|
+
this.exit(1);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// 5. Register the package row. ──────────────────────────────────────
|
|
127
|
+
printStep(`Registering package '${id}'...`);
|
|
128
|
+
const pkgBody = { manifest_id: id, display_name: displayName, visibility: flags.visibility };
|
|
129
|
+
if (flags.org)
|
|
130
|
+
pkgBody.owner_org_id = flags.org;
|
|
131
|
+
if (typeof manifest.description === 'string')
|
|
132
|
+
pkgBody.description = manifest.description;
|
|
133
|
+
const pkgRes = await this.postJson(`${baseUrl}/api/v1/cloud/packages`, pkgBody, token, flags.timeout);
|
|
134
|
+
if (!pkgRes.ok) {
|
|
135
|
+
printError(`Register package failed (${pkgRes.status}): ${pkgRes.error}`);
|
|
136
|
+
this.exit(1);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const pkg = pkgRes.body?.data ?? pkgRes.body;
|
|
140
|
+
printSuccess(`${pkg?.created ? 'Created' : 'Updated'} sys_package ${pkg?.id} (${id})`);
|
|
141
|
+
// 6. Publish the plugin version. ────────────────────────────────────
|
|
142
|
+
printStep(`Publishing version ${version}...`);
|
|
143
|
+
const verBody = {
|
|
144
|
+
version,
|
|
145
|
+
artifact_kind: 'plugin',
|
|
146
|
+
osplugin: base64,
|
|
147
|
+
plugin_manifest: manifest,
|
|
148
|
+
artifact_checksum: checksum,
|
|
149
|
+
is_pre_release: flags['pre-release'] || /-(alpha|beta|rc|dev|preview|staging|pr)/i.test(version),
|
|
150
|
+
};
|
|
151
|
+
if (signature)
|
|
152
|
+
verBody.signature = signature;
|
|
153
|
+
if (flags.note)
|
|
154
|
+
verBody.release_notes = flags.note;
|
|
155
|
+
if (flags.submit)
|
|
156
|
+
verBody.submit_for_review = true;
|
|
157
|
+
if (flags['auto-approve'])
|
|
158
|
+
verBody.auto_approve = true;
|
|
159
|
+
const verRes = await this.postJson(`${baseUrl}/api/v1/cloud/packages/${encodeURIComponent(pkg.id)}/versions`, verBody, token, flags.timeout);
|
|
160
|
+
if (!verRes.ok) {
|
|
161
|
+
printError(`Publish version failed (${verRes.status}): ${verRes.error}`);
|
|
162
|
+
const violations = Array.isArray(verRes.body?.violations) ? verRes.body.violations : [];
|
|
163
|
+
if (violations.length > 0) {
|
|
164
|
+
console.log('\n Violations:');
|
|
165
|
+
for (const v of violations)
|
|
166
|
+
console.log(` • ${v}`);
|
|
167
|
+
}
|
|
168
|
+
this.exit(1);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const ver = verRes.body?.data ?? verRes.body;
|
|
172
|
+
printSuccess('Plugin version published');
|
|
173
|
+
printKV(' Version', String(ver?.version ?? version));
|
|
174
|
+
printKV(' Listing status', String(ver?.listing_status ?? (flags.submit ? 'pending_review' : 'draft')));
|
|
175
|
+
printKV(' Artifact sha256', checksum);
|
|
176
|
+
if (!flags.submit && !flags['auto-approve'] && flags.visibility === 'marketplace') {
|
|
177
|
+
printStep('Re-run with --submit to send this version for marketplace review.');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Tiny fetch wrapper returning a normalized envelope; honours a timeout. */
|
|
181
|
+
async postJson(url, body, token, timeoutMs) {
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(url, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
188
|
+
body: JSON.stringify(body),
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
});
|
|
191
|
+
let parsed = null;
|
|
192
|
+
try {
|
|
193
|
+
parsed = await response.json();
|
|
194
|
+
}
|
|
195
|
+
catch { /* empty body */ }
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
const errMsg = parsed?.error?.message ?? parsed?.error ?? response.statusText;
|
|
198
|
+
return { ok: false, status: response.status, body: parsed, error: String(errMsg) };
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, status: response.status, body: parsed };
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return { ok: false, status: 0, body: null, error: err?.message ?? String(err) };
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (timer)
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=publish.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.js","sourceRoot":"","sources":["../../../src/commands/plugin/publish.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class PluginSign extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
artifact: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
key: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'key-id': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
out: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=sign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../../src/commands/plugin/sign.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAQ,OAAO,EAAS,MAAM,aAAa,CAAC;AAKnD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,OAAO;IAC7C,OAAgB,WAAW,SAC6C;IAExE,OAAgB,QAAQ,WAGtB;IAEF,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;MAcnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA2D3B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* `os plugin sign` — produce a publisher signature over a built `.osplugin`
|
|
4
|
+
* (ADR-0025 §3.4 step 2, framework F3).
|
|
5
|
+
*
|
|
6
|
+
* The signature is DETACHED and computed over the exact artifact bytes that
|
|
7
|
+
* will be uploaded — the same bytes the cloud control plane verifies at
|
|
8
|
+
* publish time (`verifyPublisherSignature`) and the runtime re-verifies at
|
|
9
|
+
* materialize time. It is emitted as `ed25519:<keyId>:<base64url>` and
|
|
10
|
+
* written to a `<artifact>.sig` sidecar (and printed), to be passed as the
|
|
11
|
+
* `signature` field when publishing. The artifact itself is NOT modified, so
|
|
12
|
+
* signing is idempotent and the signed bytes are exactly the built bytes.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
|
17
|
+
import { resolve as resolvePath } from 'node:path';
|
|
18
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
19
|
+
import { parseSignature, signPayload, verifyPayload } from '@objectstack/core';
|
|
20
|
+
import { printError, printHeader, printKV, printStep, printSuccess } from '../../utils/format.js';
|
|
21
|
+
import { OSPLUGIN_EXT } from '../../utils/osplugin.js';
|
|
22
|
+
export default class PluginSign extends Command {
|
|
23
|
+
static description = 'Sign a built .osplugin with a publisher Ed25519 key (ADR-0025 §3.4)';
|
|
24
|
+
static examples = [
|
|
25
|
+
'$ os plugin sign my-plugin-1.0.0.osplugin --key ./publisher.key.pem',
|
|
26
|
+
'$ os plugin sign my-plugin-1.0.0.osplugin --key ./publisher.key.pem --key-id acme-2026',
|
|
27
|
+
];
|
|
28
|
+
static args = {
|
|
29
|
+
artifact: Args.string({ description: 'Path to the .osplugin artifact', required: true }),
|
|
30
|
+
};
|
|
31
|
+
static flags = {
|
|
32
|
+
key: Flags.string({
|
|
33
|
+
char: 'k',
|
|
34
|
+
description: 'Path to the publisher Ed25519 private key (PKCS#8 PEM)',
|
|
35
|
+
required: true,
|
|
36
|
+
}),
|
|
37
|
+
'key-id': Flags.string({
|
|
38
|
+
description: 'Key identifier embedded in the signature (rotation handle)',
|
|
39
|
+
default: 'default',
|
|
40
|
+
}),
|
|
41
|
+
out: Flags.string({
|
|
42
|
+
char: 'o',
|
|
43
|
+
description: 'Output path for the detached signature (defaults to <artifact>.sig)',
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
async run() {
|
|
47
|
+
const { args, flags } = await this.parse(PluginSign);
|
|
48
|
+
printHeader('Sign Plugin');
|
|
49
|
+
const artifactPath = resolvePath(process.cwd(), args.artifact);
|
|
50
|
+
if (!existsSync(artifactPath)) {
|
|
51
|
+
printError(`Artifact not found: ${args.artifact}`);
|
|
52
|
+
this.exit(1);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!artifactPath.endsWith(OSPLUGIN_EXT)) {
|
|
56
|
+
printStep(`Warning: ${args.artifact} does not have a ${OSPLUGIN_EXT} extension`);
|
|
57
|
+
}
|
|
58
|
+
let privateKeyPem;
|
|
59
|
+
try {
|
|
60
|
+
privateKeyPem = await readFile(resolvePath(process.cwd(), flags.key), 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
printError(`Cannot read private key: ${err.message}`);
|
|
64
|
+
this.exit(1);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const artifact = new Uint8Array(await readFile(artifactPath));
|
|
68
|
+
const keyId = flags['key-id'];
|
|
69
|
+
let signature;
|
|
70
|
+
try {
|
|
71
|
+
signature = signPayload(artifact, privateKeyPem, keyId);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
printError(`Signing failed: ${err.message}`);
|
|
75
|
+
this.exit(1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Self-check: verify the freshly produced signature against the public
|
|
79
|
+
// half so a bad key / wrong format never ships silently.
|
|
80
|
+
try {
|
|
81
|
+
const pub = createPublicKey(createPrivateKey(privateKeyPem));
|
|
82
|
+
if (!verifyPayload(artifact, signature, pub)) {
|
|
83
|
+
printError('Self-verification of the produced signature failed.');
|
|
84
|
+
this.exit(1);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
printError(`Self-verification error: ${err.message}`);
|
|
90
|
+
this.exit(1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const outPath = resolvePath(process.cwd(), flags.out ?? `${args.artifact}.sig`);
|
|
94
|
+
await writeFile(outPath, signature + '\n', 'utf-8');
|
|
95
|
+
printSuccess('Plugin signed');
|
|
96
|
+
printKV(' Artifact', args.artifact);
|
|
97
|
+
printKV(' Key ID', parseSignature(signature)?.keyId ?? keyId);
|
|
98
|
+
printKV(' Signature', signature);
|
|
99
|
+
printKV(' Sidecar', flags.out ?? `${args.artifact}.sig`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=sign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.js","sourceRoot":"","sources":["../../../src/commands/plugin/sign.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,OAAO,EAAS,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,OAAO,EAAS,MAAM,aAAa,CAAC;AAwInD,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,OAAO;IACxC,OAAgB,WAAW,SAAkM;IAE7N,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;;;;;MAenB;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAEtD;IAEH;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAIpD;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA8tD3B"}
|
package/dist/commands/serve.js
CHANGED
|
@@ -8,6 +8,7 @@ import { bundleRequire } from 'bundle-require';
|
|
|
8
8
|
import { BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
|
|
9
9
|
import { shouldBootWithLibrary } from '../utils/plugin-detection.js';
|
|
10
10
|
import { readEnvWithDeprecation } from '@objectstack/types';
|
|
11
|
+
import { resolveObjectStackHome } from '@objectstack/runtime';
|
|
11
12
|
import { printError, printServerReady, } from '../utils/format.js';
|
|
12
13
|
import { CONSOLE_PATH, resolveConsolePath, hasConsoleDist, createConsoleStaticPlugin, } from '../utils/console.js';
|
|
13
14
|
import dotenvFlow from 'dotenv-flow';
|
|
@@ -1560,16 +1561,16 @@ export default class Serve extends Command {
|
|
|
1560
1561
|
}
|
|
1561
1562
|
}
|
|
1562
1563
|
}
|
|
1564
|
+
// Shared dev crypto provider for ALL of sys_secret (datasource creds
|
|
1565
|
+
// below + secret fields after start). One instance ⇒ one key, so every
|
|
1566
|
+
// encrypted secret decrypts under the same provider. Created lazily by
|
|
1567
|
+
// whichever block runs first.
|
|
1568
|
+
let sharedCryptoProvider = undefined;
|
|
1563
1569
|
// ── External Datasource Federation (ADR-0015) ─────────────────
|
|
1564
1570
|
// Federation (introspect / draft / import / validate of external
|
|
1565
|
-
// tables) ships in the open framework.
|
|
1566
|
-
// *lifecycle* (ADR-0015 Addendum — the "Add Datasource" wizard backend:
|
|
1567
|
-
// create / update / remove runtime datasources) was extracted into the
|
|
1568
|
-
// private `@objectstack/datasource-admin` package; a private host wires
|
|
1569
|
-
// its `DatasourceAdminServicePlugin` + routes after the data/metadata
|
|
1570
|
-
// services exist (see that package's README).
|
|
1571
|
+
// tables) ships in the open framework.
|
|
1571
1572
|
try {
|
|
1572
|
-
const dsMod = await import('@objectstack/service-
|
|
1573
|
+
const dsMod = await import('@objectstack/service-datasource');
|
|
1573
1574
|
const { ExternalDatasourceServicePlugin } = dsMod;
|
|
1574
1575
|
if (ExternalDatasourceServicePlugin &&
|
|
1575
1576
|
!hasPluginMatching(['service-external-datasource', 'ExternalDatasourceServicePlugin'])) {
|
|
@@ -1590,7 +1591,98 @@ export default class Serve extends Command {
|
|
|
1590
1591
|
catch (err) {
|
|
1591
1592
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1592
1593
|
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
|
|
1593
|
-
console.error(`[Datasource]
|
|
1594
|
+
console.error(`[Datasource] federation wiring failed: ${msg}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// ── Runtime Datasource Admin (ADR-0015 Addendum) ──────────────
|
|
1598
|
+
// The "Add Datasource" wizard backend: list / test / create / update /
|
|
1599
|
+
// remove datasources defined in the UI at runtime. This is open-source
|
|
1600
|
+
// *mechanism* (`@objectstack/service-datasource`); the tier line
|
|
1601
|
+
// falls on which ICryptoProvider / driver factory a host injects, not on
|
|
1602
|
+
// whether the UI can manage datasources. Mounted by default so a
|
|
1603
|
+
// self-host runtime is a complete low-code platform out of the box.
|
|
1604
|
+
//
|
|
1605
|
+
// Credentials are bound through the SAME crypto provider used for
|
|
1606
|
+
// `secret` fields below (`sharedCryptoProvider`), so every secret in
|
|
1607
|
+
// `sys_secret` (settings, secret fields, datasource creds) shares one
|
|
1608
|
+
// key. Wired BEFORE runtime.start() so the plugin's kernel:ready boot
|
|
1609
|
+
// rehydration (which decrypts persisted creds) has its binder ready.
|
|
1610
|
+
try {
|
|
1611
|
+
const adminMod = await import('@objectstack/service-datasource');
|
|
1612
|
+
const { DatasourceAdminServicePlugin, createDefaultDatasourceDriverFactory, createDatasourceSecretBinder, registerDatasourceAdminRoutes, } = adminMod;
|
|
1613
|
+
if (DatasourceAdminServicePlugin &&
|
|
1614
|
+
!hasPluginMatching(['service-datasource-admin', 'DatasourceAdminServicePlugin'])) {
|
|
1615
|
+
// Lazy data-engine surface for the secret store (resolved per call
|
|
1616
|
+
// so it works whether the engine is registered as 'data' or
|
|
1617
|
+
// 'objectql', and regardless of init ordering).
|
|
1618
|
+
const resolveEngine = () => kernel.getService?.('data') ?? kernel.getService?.('objectql');
|
|
1619
|
+
const lazySecretEngine = {
|
|
1620
|
+
insert: (o, d, opt) => resolveEngine()?.insert(o, d, opt),
|
|
1621
|
+
delete: (o, opt) => resolveEngine()?.delete(o, opt),
|
|
1622
|
+
find: (o, q) => resolveEngine()?.find(o, q),
|
|
1623
|
+
};
|
|
1624
|
+
// Fail-closed binder over the shared dev crypto provider. If the
|
|
1625
|
+
// provider can't be created, leave `secrets` undefined — the plugin
|
|
1626
|
+
// then rejects secret-bearing create/update instead of storing
|
|
1627
|
+
// cleartext (by design).
|
|
1628
|
+
let secrets = undefined;
|
|
1629
|
+
try {
|
|
1630
|
+
const { LocalCryptoProvider } = await import(
|
|
1631
|
+
/* webpackIgnore: true */ '@objectstack/service-settings');
|
|
1632
|
+
// First block to touch `sharedCryptoProvider` (still undefined
|
|
1633
|
+
// here), so create it directly; the secret-field wiring below
|
|
1634
|
+
// reuses this instance so every sys_secret shares one key.
|
|
1635
|
+
sharedCryptoProvider = new LocalCryptoProvider();
|
|
1636
|
+
secrets = createDatasourceSecretBinder({
|
|
1637
|
+
engine: lazySecretEngine,
|
|
1638
|
+
cryptoProvider: sharedCryptoProvider,
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
catch (cryptoErr) {
|
|
1642
|
+
// Best-effort fail-closed: leave `secrets` undefined so the plugin
|
|
1643
|
+
// rejects secret-bearing create/update rather than storing
|
|
1644
|
+
// cleartext. A production deployment with no stable key still
|
|
1645
|
+
// aborts boot loudly at the secret-field wiring below (where
|
|
1646
|
+
// LocalCryptoProvider's "Refusing to start in production" error is
|
|
1647
|
+
// rethrown), so we don't duplicate that abort here.
|
|
1648
|
+
console.warn(chalk.yellow(` ⚠ datasource admin: no CryptoProvider (${cryptoErr?.message ?? cryptoErr}); secret-bearing datasource create/update will fail closed`));
|
|
1649
|
+
}
|
|
1650
|
+
await kernel.use(new DatasourceAdminServicePlugin({
|
|
1651
|
+
driverFactory: createDefaultDatasourceDriverFactory(),
|
|
1652
|
+
secrets,
|
|
1653
|
+
}));
|
|
1654
|
+
trackPlugin('DatasourceAdminServicePlugin');
|
|
1655
|
+
// REST routes under /api/v1/datasources. Registered via a tiny
|
|
1656
|
+
// plugin so it resolves http.server during init (same pattern as
|
|
1657
|
+
// the hostname guard above).
|
|
1658
|
+
const adminRoutePlugin = {
|
|
1659
|
+
name: 'com.objectstack.cli.datasource-admin-routes',
|
|
1660
|
+
version: '1.0.0',
|
|
1661
|
+
init: async (ctx) => {
|
|
1662
|
+
try {
|
|
1663
|
+
const httpServer = ctx.getService?.('http.server') ?? ctx.getService?.('http-server');
|
|
1664
|
+
if (!httpServer || typeof httpServer.get !== 'function') {
|
|
1665
|
+
ctx.logger?.warn?.('[datasource-admin] http.server unavailable; REST routes not installed');
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
registerDatasourceAdminRoutes(httpServer, ctx, '/api/v1');
|
|
1669
|
+
}
|
|
1670
|
+
catch (routeErr) {
|
|
1671
|
+
ctx.logger?.warn?.(`[datasource-admin] route registration failed: ${routeErr?.message ?? routeErr}`);
|
|
1672
|
+
}
|
|
1673
|
+
},
|
|
1674
|
+
};
|
|
1675
|
+
await kernel.use(adminRoutePlugin);
|
|
1676
|
+
trackPlugin('DatasourceAdminRoutes');
|
|
1677
|
+
if (isDev) {
|
|
1678
|
+
console.log(chalk.dim(' ↪ datasource admin: runtime UI lifecycle wired (/api/v1/datasources)'));
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
catch (err) {
|
|
1683
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1684
|
+
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
|
|
1685
|
+
console.error(`[Datasource] runtime-UI admin wiring failed: ${msg}`);
|
|
1594
1686
|
}
|
|
1595
1687
|
}
|
|
1596
1688
|
// ── UI portals ────────────────────────────────────────────────
|
|
@@ -1632,30 +1724,44 @@ export default class Serve extends Command {
|
|
|
1632
1724
|
// objectql's `secret` field type encrypts on write to `sys_secret`
|
|
1633
1725
|
// and fails closed when no ICryptoProvider is registered. objectql
|
|
1634
1726
|
// must NOT depend on a crypto implementation (layering), so the
|
|
1635
|
-
// host injects one here. Dev/self-host gets
|
|
1636
|
-
//
|
|
1637
|
-
// KMS/Vault-backed provider (e.g.
|
|
1638
|
-
// dedicated plugin) before secrets are
|
|
1639
|
-
// data engine by its registered service name
|
|
1640
|
-
// `setCryptoProvider` so older engines / alternate
|
|
1641
|
-
// degrade gracefully (writing a secret then fails
|
|
1642
|
-
// designed, rather than silently storing cleartext).
|
|
1727
|
+
// host injects one here. Dev/self-host gets a LocalCryptoProvider
|
|
1728
|
+
// (AES-256-GCM keyed off `OS_SECRET_KEY` or a persisted dev key);
|
|
1729
|
+
// production hosts swap this for a KMS/Vault-backed provider (e.g.
|
|
1730
|
+
// via an env-gated branch or a dedicated plugin) before secrets are
|
|
1731
|
+
// written. We resolve the data engine by its registered service name
|
|
1732
|
+
// and feature-detect `setCryptoProvider` so older engines / alternate
|
|
1733
|
+
// data services degrade gracefully (writing a secret then fails
|
|
1734
|
+
// closed, as designed, rather than silently storing cleartext).
|
|
1643
1735
|
try {
|
|
1644
1736
|
const dataEngine = kernel.getService?.('data') ?? kernel.getService?.('objectql');
|
|
1645
1737
|
if (dataEngine && typeof dataEngine.setCryptoProvider === 'function') {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1738
|
+
if (!sharedCryptoProvider) {
|
|
1739
|
+
const { LocalCryptoProvider } = await import(
|
|
1740
|
+
/* webpackIgnore: true */ '@objectstack/service-settings');
|
|
1741
|
+
// In production LocalCryptoProvider throws when no stable key
|
|
1742
|
+
// (OS_SECRET_KEY / persisted file) is available — the fail-loud
|
|
1743
|
+
// guard against silently minting an ephemeral key and losing
|
|
1744
|
+
// every sys_secret value after a restart. Let that error be loud:
|
|
1745
|
+
// secret writes must not proceed under an unstable key.
|
|
1746
|
+
sharedCryptoProvider = new LocalCryptoProvider();
|
|
1747
|
+
}
|
|
1748
|
+
dataEngine.setCryptoProvider(sharedCryptoProvider);
|
|
1649
1749
|
if (isDev) {
|
|
1650
|
-
console.log(chalk.dim(' ↪ secret fields:
|
|
1750
|
+
console.log(chalk.dim(' ↪ secret fields: LocalCryptoProvider wired (dev) — set OS_SECRET_KEY and swap for KMS/Vault in production'));
|
|
1651
1751
|
}
|
|
1652
1752
|
}
|
|
1653
1753
|
}
|
|
1654
1754
|
catch (err) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1755
|
+
const msg = String(err?.message ?? err);
|
|
1756
|
+
if (msg.includes('Refusing to start in production')) {
|
|
1757
|
+
// Fail-loud config error: print the actionable guidance verbatim.
|
|
1758
|
+
console.error(chalk.red(msg));
|
|
1759
|
+
throw err;
|
|
1760
|
+
}
|
|
1761
|
+
// Otherwise non-fatal: without a provider, secret writes fail closed
|
|
1762
|
+
// by design. Surface a hint so operators know why a `secret` field
|
|
1657
1763
|
// write might reject.
|
|
1658
|
-
console.warn(chalk.yellow(` ⚠ secret fields: no CryptoProvider wired (${
|
|
1764
|
+
console.warn(chalk.yellow(` ⚠ secret fields: no CryptoProvider wired (${msg}); writing a secret field will fail closed`));
|
|
1659
1765
|
}
|
|
1660
1766
|
// ── Migrate-and-exit short-circuit ─────────────────────────────
|
|
1661
1767
|
// Out-of-band migration mode: the caller (e.g.
|
|
@@ -1693,6 +1799,18 @@ export default class Serve extends Command {
|
|
|
1693
1799
|
// best-effort only
|
|
1694
1800
|
}
|
|
1695
1801
|
}
|
|
1802
|
+
// Surface the dev admin seeded this boot (if any) in the banner. The
|
|
1803
|
+
// seed runs in-process during runtime.start() under serve's boot-quiet
|
|
1804
|
+
// window, so plugin-auth records the result on the `auth` service and
|
|
1805
|
+
// we print it here, after stdout is restored. Visible in both
|
|
1806
|
+
// `serve --dev` and `os dev` (the child's stdout is inherited).
|
|
1807
|
+
let seededAdmin;
|
|
1808
|
+
try {
|
|
1809
|
+
const authSvc = kernel.getService?.('auth');
|
|
1810
|
+
if (authSvc?.devSeedResult?.email)
|
|
1811
|
+
seededAdmin = authSvc.devSeedResult;
|
|
1812
|
+
}
|
|
1813
|
+
catch { /* auth service not present — nothing to show */ }
|
|
1696
1814
|
// ── Clean startup summary ──────────────────────────────────────
|
|
1697
1815
|
printServerReady({
|
|
1698
1816
|
port,
|
|
@@ -1705,7 +1823,41 @@ export default class Serve extends Command {
|
|
|
1705
1823
|
driverLabel: resolvedDriverLabel,
|
|
1706
1824
|
databaseUrl: redactDbUrl(resolvedDatabaseUrl),
|
|
1707
1825
|
multiTenant: String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false',
|
|
1826
|
+
seededAdmin,
|
|
1708
1827
|
});
|
|
1828
|
+
// ── Publish the actually-bound port ────────────────────────────
|
|
1829
|
+
// `port` here is the port the HTTP server actually bound — already
|
|
1830
|
+
// resolved past any dev auto-shift (busy 3000 → 3001). Publish it so
|
|
1831
|
+
// supervisors and the `os dev` parent never have to guess:
|
|
1832
|
+
// • IPC: when spawned with an 'ipc' channel (as `os dev` does), the
|
|
1833
|
+
// parent learns the real port without polling.
|
|
1834
|
+
// • runtime.json: a small state file under OS_HOME for external
|
|
1835
|
+
// supervisors / health checks (pid + port + url).
|
|
1836
|
+
const runtimeUrl = `http://localhost:${port}`;
|
|
1837
|
+
try {
|
|
1838
|
+
if (typeof process.send === 'function') {
|
|
1839
|
+
process.send({ type: 'objectstack:listening', port: Number(port), url: runtimeUrl });
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
catch { /* IPC channel closed — best-effort */ }
|
|
1843
|
+
try {
|
|
1844
|
+
const environmentId = process.env.OS_ENVIRONMENT_ID ?? 'env_local';
|
|
1845
|
+
const runtimeFile = path.join(resolveObjectStackHome(), `runtime.${environmentId}.json`);
|
|
1846
|
+
fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });
|
|
1847
|
+
fs.writeFileSync(runtimeFile, JSON.stringify({
|
|
1848
|
+
pid: process.pid,
|
|
1849
|
+
port: Number(port),
|
|
1850
|
+
url: runtimeUrl,
|
|
1851
|
+
environmentId,
|
|
1852
|
+
startedAt: new Date().toISOString(),
|
|
1853
|
+
}, null, 2));
|
|
1854
|
+
const cleanupRuntimeFile = () => { try {
|
|
1855
|
+
fs.rmSync(runtimeFile, { force: true });
|
|
1856
|
+
}
|
|
1857
|
+
catch { /* noop */ } };
|
|
1858
|
+
process.on('exit', cleanupRuntimeFile);
|
|
1859
|
+
}
|
|
1860
|
+
catch { /* non-fatal — supervision file is best-effort */ }
|
|
1709
1861
|
// Kernel already registers SIGINT/SIGTERM handlers during bootstrap.
|
|
1710
1862
|
// No duplicate handler needed here — just keep the process alive.
|
|
1711
1863
|
}
|