@objectstack/cli 7.5.0 → 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.
Files changed (39) hide show
  1. package/dist/commands/compile.d.ts.map +1 -1
  2. package/dist/commands/compile.js +22 -0
  3. package/dist/commands/compile.js.map +1 -1
  4. package/dist/commands/dev.d.ts +0 -10
  5. package/dist/commands/dev.d.ts.map +1 -1
  6. package/dist/commands/dev.js +30 -77
  7. package/dist/commands/dev.js.map +1 -1
  8. package/dist/commands/plugin/build.d.ts +15 -0
  9. package/dist/commands/plugin/build.d.ts.map +1 -0
  10. package/dist/commands/plugin/build.js +199 -0
  11. package/dist/commands/plugin/build.js.map +1 -0
  12. package/dist/commands/plugin/publish.d.ts +26 -0
  13. package/dist/commands/plugin/publish.d.ts.map +1 -0
  14. package/dist/commands/plugin/publish.js +211 -0
  15. package/dist/commands/plugin/publish.js.map +1 -0
  16. package/dist/commands/plugin/sign.d.ts +15 -0
  17. package/dist/commands/plugin/sign.d.ts.map +1 -0
  18. package/dist/commands/plugin/sign.js +102 -0
  19. package/dist/commands/plugin/sign.js.map +1 -0
  20. package/dist/commands/serve.d.ts.map +1 -1
  21. package/dist/commands/serve.js +175 -23
  22. package/dist/commands/serve.js.map +1 -1
  23. package/dist/utils/format.d.ts +9 -0
  24. package/dist/utils/format.d.ts.map +1 -1
  25. package/dist/utils/format.js +6 -0
  26. package/dist/utils/format.js.map +1 -1
  27. package/dist/utils/osplugin.d.ts +44 -0
  28. package/dist/utils/osplugin.d.ts.map +1 -0
  29. package/dist/utils/osplugin.js +151 -0
  30. package/dist/utils/osplugin.js.map +1 -0
  31. package/dist/utils/validate-expressions.d.ts +13 -0
  32. package/dist/utils/validate-expressions.d.ts.map +1 -0
  33. package/dist/utils/validate-expressions.js +101 -0
  34. package/dist/utils/validate-expressions.js.map +1 -0
  35. package/dist/utils/validate-expressions.test.d.ts +2 -0
  36. package/dist/utils/validate-expressions.test.d.ts.map +1 -0
  37. package/dist/utils/validate-expressions.test.js +65 -0
  38. package/dist/utils/validate-expressions.test.js.map +1 -0
  39. package/package.json +43 -45
@@ -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;AAuInD,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;CAkjD3B"}
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"}
@@ -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. The runtime-UI datasource
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-external-datasource');
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] runtime-UI lifecycle wiring failed: ${msg}`);
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 an independent
1636
- // InMemoryCryptoProvider; production hosts swap this for a
1637
- // KMS/Vault-backed provider (e.g. via an env-gated branch or a
1638
- // dedicated plugin) before secrets are written. We resolve the
1639
- // data engine by its registered service name and feature-detect
1640
- // `setCryptoProvider` so older engines / alternate data services
1641
- // degrade gracefully (writing a secret then fails closed, as
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
- const { InMemoryCryptoProvider } = await import(
1647
- /* webpackIgnore: true */ '@objectstack/service-settings');
1648
- dataEngine.setCryptoProvider(new InMemoryCryptoProvider());
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: InMemoryCryptoProvider wired (dev) — swap for KMS/Vault in production'));
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
- // Non-fatal: without a provider, secret writes fail closed by
1656
- // design. Surface a hint so operators know why a `secret` field
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 (${err?.message ?? err}); writing a secret field will fail closed`));
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
  }