@descix/cli 1.0.0 → 1.0.3

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 (103) hide show
  1. package/LICENSE.md +671 -0
  2. package/README.md +43 -4
  3. package/agent-assets/guides/README.md +2 -2
  4. package/agent-assets/guides/dynamic-site-pattern.md +29 -14
  5. package/agent-assets/guides/knowledge-only-pattern.md +5 -5
  6. package/agent-assets/guides/microservice-pattern.md +48 -4
  7. package/agent-assets/guides/static-site-pattern.md +8 -0
  8. package/agent-assets/instructions/app-asset-validation.md +5 -5
  9. package/agent-assets/instructions/onboarding-flow.md +4 -4
  10. package/agent-assets/instructions/standard-folder-structure.md +20 -11
  11. package/agent-assets/reference/app-creation.md +42 -20
  12. package/agent-assets/reference/architecture.md +24 -22
  13. package/agent-assets/reference/local-dev.md +230 -67
  14. package/agent-assets/reference/mcp-integration.md +9 -9
  15. package/agent-assets/reference/platform-api-consumer.md +244 -0
  16. package/agent-assets/reference/workspace-config.md +7 -7
  17. package/agent-assets/rules/descix_mcp.template.mdc +8 -8
  18. package/bin/descix.js +2559 -1435
  19. package/bin/kb-deny-lint.mjs +91 -0
  20. package/bin/mcp-server.js +175 -91
  21. package/lib/agent-files.js +37 -25
  22. package/lib/api-client.js +223 -56
  23. package/lib/auth-guard.js +13 -3
  24. package/lib/chat-session.js +187 -0
  25. package/lib/cli-version.js +59 -0
  26. package/lib/commands/airdrop.js +251 -0
  27. package/lib/commands/auth.js +254 -72
  28. package/lib/commands/briefer/errors.js +88 -0
  29. package/lib/commands/briefer/index.js +319 -0
  30. package/lib/commands/briefer/sources/canonical-sources.js +129 -0
  31. package/lib/commands/briefer/sources/entitlements.js +209 -0
  32. package/lib/commands/briefer/sources/environments.js +205 -0
  33. package/lib/commands/briefer/sources/identifiers.js +214 -0
  34. package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
  35. package/lib/commands/briefer/sources/routing.js +275 -0
  36. package/lib/commands/briefer/sources/what-is-not.js +223 -0
  37. package/lib/commands/briefer/stitcher.js +134 -0
  38. package/lib/commands/briefer/util/source-reader.js +502 -0
  39. package/lib/commands/config.js +210 -90
  40. package/lib/commands/corpus.js +978 -0
  41. package/lib/commands/credits.js +122 -0
  42. package/lib/commands/health.js +627 -0
  43. package/lib/commands/kb-list-render.js +32 -0
  44. package/lib/commands/kb.js +190 -413
  45. package/lib/commands/mcp.js +5 -5
  46. package/lib/commands/microservice-restart.js +359 -0
  47. package/lib/commands/model-config.js +313 -0
  48. package/lib/commands/retired-kb-sync.js +134 -0
  49. package/lib/commands/serve.js +26 -1
  50. package/lib/commands/status.js +23 -51
  51. package/lib/commands/update.js +36 -107
  52. package/lib/core/Chunker.js +491 -17
  53. package/lib/core/CorpusDenyLint.js +326 -0
  54. package/lib/core/CorpusWalker.js +357 -0
  55. package/lib/core/Hydrator.js +75 -29
  56. package/lib/core/ManifestLoader.js +533 -0
  57. package/lib/core/SiteWalker.js +218 -0
  58. package/lib/core/Syncer.js +163 -11
  59. package/lib/diagnostic-log.js +41 -0
  60. package/lib/environment-report.js +100 -0
  61. package/lib/global-config.js +4 -1
  62. package/lib/oauth-client.js +164 -0
  63. package/lib/origin.js +143 -0
  64. package/lib/service-api-client.js +95 -0
  65. package/lib/wallet-environment.js +49 -0
  66. package/lib/wallet-file.js +16 -2
  67. package/lib/wizard/setup.js +10 -9
  68. package/lib/workspace-config.js +380 -235
  69. package/lib/workspace-identity.js +92 -0
  70. package/lib/workspace-utils.js +41 -61
  71. package/package.json +29 -9
  72. package/templates/agent-claude.md +157 -9
  73. package/templates/agent-cline.md +68 -8
  74. package/templates/agent-copilot.md +68 -8
  75. package/templates/agent-cursor.md +68 -8
  76. package/templates/alignment-schema.json +20 -0
  77. package/templates/cloud-brain-hook.sh +20 -0
  78. package/templates/drive/README.md +1 -1
  79. package/templates/drive_templates/README.md +2 -2
  80. package/templates/scaffolds/README.md +6 -10
  81. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +30 -0
  82. package/templates/scaffolds/microservice/app.js +51 -10
  83. package/templates/scaffolds/microservice/app.yaml +2 -2
  84. package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
  85. package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
  86. package/templates/scaffolds/microservice/manifest.json +0 -1
  87. package/templates/scaffolds/microservice/package.json +3 -3
  88. package/templates/scaffolds/microservice/scripts/register.js +56 -41
  89. package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
  90. package/templates/scaffolds/microservice/services/utils.js +20 -238
  91. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
  92. package/SERVICE_README_sdk.md +0 -399
  93. package/descix-cli-1.0.0.tgz +0 -0
  94. package/lib/commands/app-wizard.js +0 -627
  95. package/lib/commands/folder.js +0 -291
  96. package/lib/core/PathContext.js +0 -335
  97. package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
  98. package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
  99. package/templates/scaffolds/site/README.md +0 -51
  100. package/templates/scaffolds/site/app.js +0 -22
  101. package/templates/scaffolds/site/index.html +0 -42
  102. package/templates/scaffolds/site/styles.css +0 -123
  103. package/test-hydration.js +0 -150
@@ -0,0 +1,164 @@
1
+ /**
2
+ * DeSciX CLI OAuth client — WS-HEADLESS-MVP-A1
3
+ * (mcp-oauth-longlived-tokens-design-2026-06-30.md §2.1/§2.2)
4
+ *
5
+ * The gcloud-ADC-style long-lived credential layer for the CLI/MCP:
6
+ * - PKCE S256 pair generation (binds code redemption to THIS process)
7
+ * - Dynamic Client Registration (RFC 7591) for the CLI public client
8
+ * - authorization_code redemption (the device→OAuth bridge hands the code
9
+ * back through device_check_status — no browser redirect surface)
10
+ * - refresh_token grant (silent refresh; the AS rotates on use)
11
+ * - RFC 7009 revocation (descix logout)
12
+ *
13
+ * These are the OAuth AS's OWN HTTP endpoints (/oauth/*), NOT /apifront commands —
14
+ * the CLI POSTs to them directly, exactly as any OAuth client would (design §5:
15
+ * "CLI-is-HTTP-only — the CLI just POSTs to /oauth/token"). Everything else in the
16
+ * CLI still routes /apifront/ via DeSciXApiClient.
17
+ */
18
+
19
+ import crypto from 'crypto';
20
+ import axios from 'axios';
21
+
22
+ /** OOB redirect URI for the CLI public client — the bridge returns the code via the
23
+ * device-status poll, never via redirect, but the OAuth records still carry a
24
+ * registered redirect_uri which redemption must echo (RFC 6749 §4.1.3). */
25
+ export const OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
26
+
27
+ /** Default scope requested by `descix login`. The scope is a CEILING, not a grant:
28
+ * write/admin are honored only on internal-posture deployments and, even there, the
29
+ * server's checkCommandPermission decides per-user. On public deployments the wider
30
+ * scopes are inert by construction (design §2.3). */
31
+ export const DEFAULT_OAUTH_SCOPE = 'mcp:read mcp:tools mcp:write mcp:admin';
32
+
33
+ /** Generate a PKCE S256 verifier/challenge pair. */
34
+ export function generatePkcePair() {
35
+ const verifier = crypto.randomBytes(32).toString('base64url');
36
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
37
+ return { verifier, challenge };
38
+ }
39
+
40
+ /** Shared axios config for the AS endpoints (self-signed local dev handling
41
+ * mirrors DeSciXApiClient). */
42
+ async function axiosConfigFor(baseUrl) {
43
+ const config = { timeout: 30000, validateStatus: () => true };
44
+ if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) {
45
+ const https = await import('https');
46
+ config.httpsAgent = new https.Agent({ rejectUnauthorized: false });
47
+ }
48
+ return config;
49
+ }
50
+
51
+ /**
52
+ * Register the CLI as an OAuth public client (RFC 7591 DCR).
53
+ * @returns {Promise<string>} client_id
54
+ */
55
+ export async function registerOAuthClient(baseUrl, { clientName = 'descix-cli' } = {}) {
56
+ const config = await axiosConfigFor(baseUrl);
57
+ const resp = await axios.post(`${baseUrl}/oauth/register`, {
58
+ client_name: clientName,
59
+ redirect_uris: [OOB_REDIRECT_URI],
60
+ grant_types: ['authorization_code', 'refresh_token'],
61
+ response_types: ['code'],
62
+ token_endpoint_auth_method: 'none',
63
+ }, config);
64
+ if (resp.status !== 201 || !resp.data?.client_id) {
65
+ throw new Error(`OAuth client registration failed (HTTP ${resp.status}): ${JSON.stringify(resp.data)}`);
66
+ }
67
+ return resp.data.client_id;
68
+ }
69
+
70
+ /** POST /oauth/token — shared form-encoded grant helper. */
71
+ async function tokenGrant(baseUrl, form) {
72
+ const config = await axiosConfigFor(baseUrl);
73
+ config.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
74
+ const resp = await axios.post(`${baseUrl}/oauth/token`, new URLSearchParams(form).toString(), config);
75
+ if (resp.status !== 200 || !resp.data?.access_token) {
76
+ const err = new Error(`OAuth token grant '${form.grant_type}' failed (HTTP ${resp.status}): ` +
77
+ `${resp.data?.error || 'unknown'} — ${resp.data?.error_description || JSON.stringify(resp.data)}`);
78
+ err.oauthError = resp.data?.error || null;
79
+ err.httpStatus = resp.status;
80
+ throw err;
81
+ }
82
+ return resp.data; // { access_token, token_type, expires_in, refresh_token, scope }
83
+ }
84
+
85
+ /**
86
+ * Redeem the device-bridge authorization code (PKCE) for the token pair.
87
+ * @returns {Promise<Object>} { access_token, expires_in, refresh_token, scope }
88
+ */
89
+ export async function redeemAuthorizationCode(baseUrl, { code, clientId, codeVerifier }) {
90
+ return tokenGrant(baseUrl, {
91
+ grant_type: 'authorization_code',
92
+ code,
93
+ redirect_uri: OOB_REDIRECT_URI,
94
+ client_id: clientId,
95
+ code_verifier: codeVerifier,
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Silent refresh (gcloud-ADC analogue). The AS ROTATES the refresh token on use —
101
+ * callers MUST persist BOTH returned tokens or the next refresh trips reuse-detection
102
+ * and revokes the whole chain.
103
+ * @returns {Promise<Object>} { access_token, expires_in, refresh_token, scope }
104
+ */
105
+ export async function refreshOAuthTokens(baseUrl, { refreshToken, clientId }) {
106
+ return tokenGrant(baseUrl, {
107
+ grant_type: 'refresh_token',
108
+ refresh_token: refreshToken,
109
+ client_id: clientId,
110
+ });
111
+ }
112
+
113
+ /**
114
+ * RFC 7009 revocation (used by `descix logout`). 200 is returned even for
115
+ * unknown tokens; only a transport/server failure throws.
116
+ */
117
+ export async function revokeOAuthToken(baseUrl, { token, clientId }) {
118
+ const config = await axiosConfigFor(baseUrl);
119
+ config.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
120
+ const resp = await axios.post(`${baseUrl}/oauth/revoke`, new URLSearchParams({
121
+ token,
122
+ token_type_hint: 'refresh_token',
123
+ ...(clientId ? { client_id: clientId } : {}),
124
+ }).toString(), config);
125
+ if (resp.status !== 200) {
126
+ throw new Error(`OAuth revocation failed (HTTP ${resp.status}): ${JSON.stringify(resp.data)}`);
127
+ }
128
+ return true;
129
+ }
130
+
131
+ /**
132
+ * Build the persisted `oauth` credential block for wallet.json from a token response.
133
+ * Single shape owner — ensureSession/login/logout all consume this contract.
134
+ */
135
+ export function buildOAuthCredentialBlock({ clientId, tokens, tokenEndpointBase, scope }) {
136
+ return {
137
+ client_id: clientId,
138
+ access_token: tokens.access_token,
139
+ access_token_expires_at: new Date(Date.now() + (tokens.expires_in * 1000)).toISOString(),
140
+ refresh_token: tokens.refresh_token,
141
+ scope: tokens.scope || scope || null,
142
+ token_endpoint: `${tokenEndpointBase}/oauth/token`,
143
+ };
144
+ }
145
+
146
+ /** Is the cached OAuth access token still fresh (with clock-skew margin)? */
147
+ export function isOAuthAccessTokenFresh(oauthBlock, skewMs = 60000) {
148
+ if (!oauthBlock?.access_token || !oauthBlock?.access_token_expires_at) return false;
149
+ const expiresAt = Date.parse(oauthBlock.access_token_expires_at);
150
+ if (Number.isNaN(expiresAt)) return false;
151
+ return (expiresAt - Date.now()) > skewMs;
152
+ }
153
+
154
+ export default {
155
+ OOB_REDIRECT_URI,
156
+ DEFAULT_OAUTH_SCOPE,
157
+ generatePkcePair,
158
+ registerOAuthClient,
159
+ redeemAuthorizationCode,
160
+ refreshOAuthTokens,
161
+ revokeOAuthToken,
162
+ buildOAuthCredentialBlock,
163
+ isOAuthAccessTokenFresh,
164
+ };
package/lib/origin.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * THE ONE OWNER OF "which DeSciX origin does this invocation talk to, and who chose it".
3
+ *
4
+ * WHY (measured 2026-08-29, published @descix/cli 1.0.1): `getApiUrl()` on an unconfigured
5
+ * workspace returned the production origin and said NOTHING. A developer who had configured
6
+ * nothing and a developer who had deliberately chosen production produced byte-identical
7
+ * results, and the CLI then reported that origin back in agent-instruction files, in
8
+ * `config show`, and in the wallet it minted.
9
+ *
10
+ * THE RULE THIS ENFORCES (contract I1 as amended by (A'), rev 2, 2026-08-30):
11
+ * · An unconfigured workspace resolves to the DECLARED DEFAULT, production. That is the
12
+ * shipped product's target and CEO-D-2026-08-18 stands. The PROD target was never the
13
+ * defect.
14
+ * · SILENCE was the defect. Every resolution carries its SOURCE, and every network-bound
15
+ * command PRINTS it — always, not only on the default. "Chose prod" and "chose nothing"
16
+ * are distinguished by `source`, never by a null nobody sees.
17
+ * · A CONFIGURED-but-invalid origin still FAILS LOUD, naming the fix. Landing on the default
18
+ * is a product decision; silently discarding something the developer actually typed is not.
19
+ *
20
+ * NO SECOND SPELLING OF THE DEFAULT. The origin table has ONE owner, `@descix/app-sdk/dev`,
21
+ * and this module IMPORTS the production origin from it rather than writing the literal down.
22
+ * That is deliberate and it is load-bearing for acceptance A1: `grep` of the packed tarball for
23
+ * the production literal outside the one owner returns 0 because there is no second copy to
24
+ * find -- not even here.
25
+ *
26
+ * This file imports nothing from the CLI, so it can be consumed by `workspace-config.js`
27
+ * without a cycle.
28
+ */
29
+ import { DEFAULT_API_URL } from '@descix/app-sdk/dev';
30
+
31
+ /**
32
+ * The single spelling of "are these two origins the same origin" — trailing slashes and
33
+ * surrounding whitespace do not make an origin different.
34
+ *
35
+ * Exported because other modules compare origins against ENV_MAP and MUST use the same
36
+ * comparison this module resolves with; a second private copy is how the two drift.
37
+ *
38
+ * @param {string} origin
39
+ * @returns {string}
40
+ */
41
+ export function normalizeOrigin(origin) {
42
+ return String(origin).trim().replace(/\/+$/, '');
43
+ }
44
+
45
+ /**
46
+ * Thrown when a source NAMED an origin and that origin is not usable. Carries a `code` so
47
+ * callers can recognise it without matching on message text.
48
+ *
49
+ * This is the loud failure I1 requires. It replaced `OriginUnresolvedError`, which fired when
50
+ * NOTHING was configured — a case that is no longer a failure at all under (A'), because the
51
+ * declared default answers it. The old error was DELETED rather than fenced: an unreachable
52
+ * error path kept "just in case" is the compat fence CEO-D-2026-07-26 forbids.
53
+ */
54
+ export class OriginInvalidError extends Error {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = 'OriginInvalidError';
58
+ this.code = 'ORIGIN_INVALID';
59
+ }
60
+ }
61
+
62
+ /**
63
+ * The resolution order, most explicit first. Each entry is [human-readable source, key].
64
+ * The ORDER is the contract; the array is the only place it is written down.
65
+ */
66
+ const PRECEDENCE = [
67
+ ['DESCIX_API_URL (or --api-url / --env)', 'envVar'],
68
+ ['.descix/workspace.json env.apiUrl', 'workspaceEnvApiUrl'],
69
+ ['.descix/workspace.json apiUrl (legacy)', 'legacyApiUrl'],
70
+ ['~/.descix/config.json api_url', 'globalApiUrl'],
71
+ ];
72
+
73
+ /** The remedy text every failure prints. One spelling, so every surface says the same thing. */
74
+ export const ORIGIN_REMEDY =
75
+ 'Choose one explicitly:\n' +
76
+ ' descix config init --env dev|demo|prod (persists env.apiUrl in .descix/workspace.json)\n' +
77
+ ' descix --env dev|demo|prod <command> (this invocation only)\n' +
78
+ ' export DESCIX_API_URL=https://... (this shell only)';
79
+
80
+ /**
81
+ * The SOURCE label for a resolution nobody configured. Exported so the printed env line, the
82
+ * tests and any consumer all use one spelling instead of three that drift.
83
+ *
84
+ * The wording is the contract's own (I1, rev 2): it names the state AND the one command that
85
+ * changes it, because a developer who reads "prod" and does not want prod needs the fix in the
86
+ * same line, not in a doc.
87
+ */
88
+ export const DEFAULT_ORIGIN_SOURCE =
89
+ 'default — no workspace config; `descix config init --env dev` targets DEV';
90
+
91
+ /**
92
+ * Is this string usable as an API origin? Absolute http(s) URLs only.
93
+ * @param {string} value
94
+ * @returns {boolean}
95
+ */
96
+ function isUsableOrigin(value) {
97
+ let parsed;
98
+ try {
99
+ parsed = new URL(String(value).trim());
100
+ } catch {
101
+ return false;
102
+ }
103
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
104
+ }
105
+
106
+ /**
107
+ * Resolve the API origin from the sources a caller has in hand.
108
+ *
109
+ * @param {object} sources
110
+ * @param {string|null} [sources.envVar] process.env.DESCIX_API_URL
111
+ * @param {string|null} [sources.workspaceEnvApiUrl] workspace.json env.apiUrl
112
+ * @param {string|null} [sources.legacyApiUrl] workspace.json top-level apiUrl
113
+ * @param {string|null} [sources.globalApiUrl] ~/.descix/config.json api_url
114
+ * @returns {{ origin: string, source: string, isDefault: boolean }} the origin AND which source
115
+ * supplied it — the source is returned because "where did this come from" is the
116
+ * question every misreport in this contract failed to answer. It NEVER returns null
117
+ * and it never throws for an absent configuration.
118
+ * @throws {OriginInvalidError} when a source NAMED an origin that cannot be used. Falling back
119
+ * to the default here would silently discard what the developer typed, which is the
120
+ * same class of defect from the other direction.
121
+ */
122
+ export function resolveOrigin(sources = {}) {
123
+ for (const [label, key] of PRECEDENCE) {
124
+ const value = sources[key];
125
+ if (typeof value === 'string' && value.trim() !== '') {
126
+ if (!isUsableOrigin(value)) {
127
+ throw new OriginInvalidError(
128
+ `The DeSciX API origin configured in ${label} is not a usable origin: "${String(value).trim()}".\n` +
129
+ 'An origin must be an absolute http(s) URL, e.g. https://dev.descix.net.\n\n' +
130
+ ORIGIN_REMEDY,
131
+ );
132
+ }
133
+ return { origin: normalizeOrigin(value), source: label, isDefault: false };
134
+ }
135
+ }
136
+ // Nothing was configured. That is not a failure — it is the shipped product's declared
137
+ // target, and the caller is required to PRINT that it landed here.
138
+ return {
139
+ origin: normalizeOrigin(DEFAULT_API_URL),
140
+ source: DEFAULT_ORIGIN_SOURCE,
141
+ isDefault: true,
142
+ };
143
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Service-context DeSciX api-client — the CLI's REST api-client, packaged for reuse inside
3
+ * an APP MICROSERVICE running in the cloud.
4
+ *
5
+ * AUTH MODEL (CEO-D-2026-06-02-APP-MICROSERVICE-IS-CLI-CLIENT-WALLET-SIG):
6
+ * "An app microservice is just THE CLI's REST api-client running in the cloud." It
7
+ * authenticates to /apifront EXACTLY like the CLI does — it holds the DEVELOPER'S OWN
8
+ * durable credential (wallet_address + the `signature` from .descix/wallet.json), presents
9
+ * it to `reconnect_by_wallet` to mint a session access_token, and makes /apifront calls AS
10
+ * THE DEVELOPER. There is NO new apifront auth path: this is the existing CLI wallet-signature
11
+ * -> session path (api-client buildRequestBody + ensureSession -> reconnect_by_wallet).
12
+ * NO SA-OIDC, NO delegate signature / X-NFT-ID / SERVICE_KEY, NO Signer, NO OWNER_SIGNATURE.
13
+ *
14
+ * Difference from the CLI: a Cloud Run service has no `.descix/wallet.json` and no TTY. So:
15
+ * - the developer credential is supplied EXPLICITLY (read from the service's config — the
16
+ * GITIGNORED dev-overrides.json in dev / a SECRET in prod, NEVER checked-in
17
+ * defaults-config.json, because the signature is a credential), and
18
+ * - on a session-refresh failure it HARD-FAILS (serviceMode) — it never tries interactive
19
+ * device login.
20
+ *
21
+ * The credential is the developer's own / their responsibility (their API call), same risk
22
+ * profile as wallet.json. HARD-FAIL (no fallback) if wallet_address or signature is absent.
23
+ */
24
+
25
+ import { DeSciXApiClient } from './api-client.js';
26
+
27
+ /**
28
+ * Build a headless service-context api-client bound to a Core broker URL and the developer's
29
+ * durable wallet credential.
30
+ *
31
+ * @param {object} cfg
32
+ * @param {string} cfg.baseUrl - Core broker base URL (e.g. utils.CORE_API_URL). The
33
+ * api-client appends `/apifront/`. Required.
34
+ * @param {string} cfg.walletAddress - The developer's wallet address. Required.
35
+ * @param {string} cfg.signature - The developer's durable `signature` (from their
36
+ * .descix/wallet.json). Required.
37
+ * @param {string} [cfg.userId] - Optional developer user_id (resolved by reconnect_by_wallet
38
+ * if omitted).
39
+ * @returns {{ client: DeSciXApiClient, invoke: (command: string, params?: object) => Promise<any> }}
40
+ * `client` is the underlying api-client; `invoke(command, params)` returns Core's
41
+ * `message` payload (the same shape `fetchAppAsset`'s invoker expects).
42
+ */
43
+ export function createServiceApiClient({ baseUrl, walletAddress, signature, userId } = {}) {
44
+ // The api-client appends `/apifront/`; accept either a bare origin or a `.../apifront` URL
45
+ // and normalize to the origin so we never double-append.
46
+ if (!baseUrl || typeof baseUrl !== 'string') {
47
+ throw new Error(
48
+ 'createServiceApiClient: baseUrl is required (the Core broker URL, e.g. utils.CORE_API_URL). No fallback.'
49
+ );
50
+ }
51
+ if (!walletAddress || typeof walletAddress !== 'string') {
52
+ throw new Error(
53
+ 'createServiceApiClient: walletAddress is required — the developer credential. Put it in the ' +
54
+ 'GITIGNORED dev-overrides.json (dev) / a secret (prod), never defaults-config.json. No fallback.'
55
+ );
56
+ }
57
+ if (!signature || typeof signature !== 'string') {
58
+ throw new Error(
59
+ 'createServiceApiClient: signature is required — the developer durable credential (from their ' +
60
+ '.descix/wallet.json). Put it in the GITIGNORED dev-overrides.json (dev) / a secret (prod), ' +
61
+ 'never defaults-config.json. No fallback.'
62
+ );
63
+ }
64
+
65
+ const origin = baseUrl.replace(/\/apifront\/?$/, '').replace(/\/$/, '');
66
+
67
+ const client = new DeSciXApiClient({ baseUrl: origin, serviceMode: true });
68
+ // Provide the developer credential directly — NO wallet-file / workspace walk in a service.
69
+ client.setCredentials({
70
+ userId: userId || null,
71
+ accessToken: null, // minted on first call via reconnect_by_wallet
72
+ walletAddress,
73
+ signature,
74
+ communityId: null,
75
+ tokenSymbol: null,
76
+ });
77
+ // Mark initialized so the client never tries to discover a workspace/wallet on disk.
78
+ client._initialized = true;
79
+
80
+ /**
81
+ * Invoke a Core command AS the developer. Returns Core's `message` payload on success
82
+ * (matching the meshLoopback/fetchAppAsset contract it replaces).
83
+ */
84
+ async function invoke(command, params = {}) {
85
+ if (!command || typeof command !== 'string') {
86
+ throw new Error('serviceApiClient.invoke: command (string) is required.');
87
+ }
88
+ const data = await client.invoke(command, params);
89
+ return data?.message ?? data;
90
+ }
91
+
92
+ return { client, invoke };
93
+ }
94
+
95
+ export default createServiceApiClient;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * THE ONE OWNER OF "which environment was this credential obtained against".
3
+ *
4
+ * WHY (measured 2026-08-29): wallet.json carries no environment field at all, so a DEV
5
+ * credential is byte-indistinguishable from a PROD one. Nothing downstream can tell a developer
6
+ * that the session in their workspace was minted somewhere other than where they are pointing.
7
+ *
8
+ * SCOPE — WRITE ONLY, DELIBERATELY. This module RECORDS the origin a wallet was obtained
9
+ * against. It does NOT refuse a mismatched wallet, and nothing here should grow into a check.
10
+ * Enforcement needs a migration story for every wallet already on disk WITHOUT this field
11
+ * (they are all of them, today), and shipping half an enforcement inside a publish is how a
12
+ * guard that fires on correct code gets born. Enforcement is a separate row.
13
+ *
14
+ * The environment NAME comes from `environment-report.js::environmentNameFor` — the one owner
15
+ * of that derivation, shared with the printed env line — so this file holds no second copy of
16
+ * either the origin table or the matching rule.
17
+ */
18
+ // The origin -> env-name derivation used to live in this file. It now has ONE owner, consumed
19
+ // here and by the printed env line, because two derivations of one fact is the mirror drift
20
+ // this contract exists to end.
21
+ import { environmentNameFor } from './environment-report.js';
22
+ import { normalizeOrigin } from './origin.js';
23
+
24
+ /**
25
+ * The environment stamp to persist alongside a freshly obtained credential.
26
+ *
27
+ * @param {string} apiUrl - the origin the credential was actually obtained against
28
+ * (i.e. `apiClient.baseUrl` at the moment of the exchange)
29
+ * @returns {{ apiUrl: string, environment: string }} `environment` is the ENV_MAP name whose
30
+ * url matches (`dev` | `demo` | `prod`), or `'custom'` when the origin is a real one
31
+ * that simply is not in the map — a self-hosted or port-forwarded gateway.
32
+ *
33
+ * @throws when called with no origin. There is no default: guessing which environment a
34
+ * credential came from is exactly the misreport this contract exists to end, and every
35
+ * caller has the origin in hand.
36
+ */
37
+ export function walletEnvironmentStamp(apiUrl) {
38
+ if (!apiUrl || String(apiUrl).trim() === '') {
39
+ throw new Error(
40
+ 'walletEnvironmentStamp: refusing to stamp a credential with no origin — ' +
41
+ 'the environment a wallet was obtained against cannot be guessed.',
42
+ );
43
+ }
44
+
45
+ return {
46
+ apiUrl: normalizeOrigin(apiUrl),
47
+ environment: environmentNameFor(apiUrl),
48
+ };
49
+ }
@@ -37,7 +37,15 @@ export class WalletFileManager {
37
37
  try {
38
38
  const data = await fs.readFile(walletPath, 'utf-8');
39
39
  const walletData = JSON.parse(data);
40
-
40
+
41
+ // WS-HEADLESS-MVP-A1 §2.5 (parent plan §10.4) — API_KEY is the vocabulary-aligned
42
+ // ALIAS of the wallet signature (the durable headless bearer credential). Alias-only
43
+ // (CEO §7 Q4): `signature` stays canonical on disk; a file authored with only
44
+ // API_KEY is normalized on read.
45
+ if (walletData && walletData.API_KEY && !walletData.signature) {
46
+ walletData.signature = walletData.API_KEY;
47
+ }
48
+
41
49
  if (this.validateWalletFile(walletData)) {
42
50
  return walletData;
43
51
  } else {
@@ -66,13 +74,19 @@ export class WalletFileManager {
66
74
  throw new Error('Invalid wallet data structure');
67
75
  }
68
76
 
77
+ // WS-HEADLESS-MVP-A1 §2.5 — keep the API_KEY alias mirrored to the canonical
78
+ // signature so the on-disk file self-documents the alias (alias-only rename).
79
+ if (walletData.signature) {
80
+ walletData.API_KEY = walletData.signature;
81
+ }
82
+
69
83
  // Ensure directory exists
70
84
  const dir = path.dirname(walletPath);
71
85
  await fs.mkdir(dir, { recursive: true });
72
86
 
73
87
  // Write file with restricted permissions (600)
74
88
  await fs.writeFile(walletPath, JSON.stringify(walletData, null, 2), { mode: 0o600 });
75
-
89
+
76
90
  return true;
77
91
  } catch (error) {
78
92
  console.error(`[WalletFile] Error saving wallet file: ${error.message}`);
@@ -22,6 +22,7 @@ import chalk from 'chalk';
22
22
  import ora from 'ora';
23
23
  import inquirer from 'inquirer';
24
24
  import { DeSciXApiClient } from '../api-client.js';
25
+ import { ENV_ORIGINS } from '@descix/app-sdk/dev';
25
26
  import { requireAuth, isAuthenticated } from '../auth-guard.js';
26
27
  import * as authCommands from '../commands/auth.js';
27
28
  import * as fs from 'fs/promises';
@@ -116,7 +117,7 @@ async function generateRulesFile(workspacePath, entitlements, primaryCommunity)
116
117
  // Customize with workspace-specific values
117
118
  const customized = template
118
119
  .replace(/\{\{SDK_ASSETS_PATH\}\}/g, '.descix/sdk-assets')
119
- .replace(/\{\{PRIMARY_COMMUNITY\}\}/g, primaryCommunity || 'descix')
120
+ .replace(/\{\{PRIMARY_COMMUNITY\}\}/g, primaryCommunity || 'daita')
120
121
  .replace(/\{\{ENTITLEMENTS_JSON\}\}/g, JSON.stringify(entitlementsSummary, null, 2))
121
122
  .replace(/\{\{SDK_VERSION\}\}/g, SDK_VERSION)
122
123
  .replace(/\{\{WORKSPACE_PATH\}\}/g, workspacePath);
@@ -148,7 +149,7 @@ async function autoRegisterSdkService(apiClient) {
148
149
  // Vectorize for tell_me_how discovery
149
150
  await apiClient.invoke('vectorize_service_readme', {
150
151
  service_name: 'sdk',
151
- community_id: 'descix',
152
+ community_id: 'daita',
152
153
  app_id: 'docs',
153
154
  readme_content: readmeContent
154
155
  });
@@ -216,7 +217,7 @@ export async function runSetupWizard(options = {}) {
216
217
  name: 'environment',
217
218
  message: 'Which environment are you connecting to?',
218
219
  choices: [
219
- { name: 'Production (app.descix.net)', value: 'production' },
220
+ { name: `Production (${ENV_ORIGINS.prod})`, value: 'production' },
220
221
  { name: 'Local Development (localhost:4000)', value: 'local' },
221
222
  { name: 'Custom URL', value: 'custom' }
222
223
  ],
@@ -225,7 +226,7 @@ export async function runSetupWizard(options = {}) {
225
226
  ]);
226
227
 
227
228
  if (environment === 'production') {
228
- apiUrl = 'https://descix.net';
229
+ apiUrl = ENV_ORIGINS.prod;
229
230
  } else if (environment === 'local') {
230
231
  apiUrl = 'https://localhost:4000';
231
232
  } else {
@@ -348,9 +349,9 @@ export async function runSetupWizard(options = {}) {
348
349
  }
349
350
 
350
351
  // Derive primary community from entitlements
351
- let primaryCommunity = 'descix';
352
+ let primaryCommunity = 'daita';
352
353
  if (entitlements.communities.length > 0) {
353
- primaryCommunity = entitlements.communities[0].community_id || entitlements.communities[0].id || 'descix';
354
+ primaryCommunity = entitlements.communities[0].community_id || entitlements.communities[0].id || 'daita';
354
355
  }
355
356
 
356
357
  // Step 4: MCP Configuration
@@ -443,7 +444,7 @@ export async function runSetupWizard(options = {}) {
443
444
  additionalContexts: [],
444
445
  defaultContext: {
445
446
  communityId: primaryCommunity,
446
- appId: 'agent',
447
+ appId: 'daita',
447
448
  kbId: 'General'
448
449
  },
449
450
  apiUrl: apiUrl,
@@ -490,8 +491,8 @@ export async function runSetupWizard(options = {}) {
490
491
  console.log(chalk.white('If MCP fails, use CLI directly:'));
491
492
  console.log(chalk.gray(' descix purchases # View your entitlements'));
492
493
  console.log(chalk.gray(' descix tell-me-how "question" # Discover tools'));
493
- console.log(chalk.gray(' descix sync assets -c X -a Y # Sync app assets'));
494
- console.log(chalk.gray(' descix sync kb -c X -a Y # Sync knowledge base\n'));
494
+ console.log(chalk.gray(' descix app sync-assets -a Y # Sync app assets'));
495
+ console.log(chalk.gray(' descix kb corpus sync -a Y # Sync knowledge base\n'));
495
496
 
496
497
  } catch (error) {
497
498
  spinner.fail('Setup failed');