@harperfast/harper 5.2.0-alpha.4 → 5.2.0-alpha.6
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/agent/agent.ts +152 -10
- package/agent/bestPractices.ts +58 -0
- package/agent/mcpTools.ts +122 -0
- package/agent/operations.ts +10 -1
- package/agent/registryTools.ts +117 -0
- package/agent/session.ts +16 -7
- package/agent/tools/fsTools.ts +74 -27
- package/agent/tools/inspectorTool.ts +459 -0
- package/agent/toolset.ts +27 -7
- package/agent/types.ts +2 -0
- package/components/Application.ts +566 -85
- package/components/OptionsWatcher.ts +62 -2
- package/components/Scope.ts +31 -8
- package/components/componentLoader.ts +5 -1
- package/components/componentSecrets.ts +514 -55
- package/components/deploymentRecorder.ts +7 -6
- package/components/gitCredentialHelper.js +115 -0
- package/components/gitCredentialServer.ts +251 -0
- package/components/mcp/tools/application.ts +197 -23
- package/components/mcp/tools/operations.ts +1 -1
- package/components/mcp/tools/schemas/derive.ts +6 -2
- package/components/operations.js +21 -19
- package/components/operationsValidation.js +86 -21
- package/components/secretOperations.ts +110 -30
- package/config/harperConfigEnvVars.ts +90 -5
- package/config-root.schema.json +4 -0
- package/dist/agent/agent.d.ts +24 -3
- package/dist/agent/agent.js +172 -10
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/bestPractices.d.ts +24 -0
- package/dist/agent/bestPractices.js +60 -0
- package/dist/agent/bestPractices.js.map +1 -0
- package/dist/agent/mcpTools.d.ts +24 -0
- package/dist/agent/mcpTools.js +115 -0
- package/dist/agent/mcpTools.js.map +1 -0
- package/dist/agent/operations.js +10 -1
- package/dist/agent/operations.js.map +1 -1
- package/dist/agent/registryTools.d.ts +45 -0
- package/dist/agent/registryTools.js +113 -0
- package/dist/agent/registryTools.js.map +1 -0
- package/dist/agent/session.js +16 -7
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/tools/fsTools.js +70 -28
- package/dist/agent/tools/fsTools.js.map +1 -1
- package/dist/agent/tools/inspectorTool.d.ts +40 -0
- package/dist/agent/tools/inspectorTool.js +428 -0
- package/dist/agent/tools/inspectorTool.js.map +1 -0
- package/dist/agent/toolset.d.ts +16 -6
- package/dist/agent/toolset.js +17 -7
- package/dist/agent/toolset.js.map +1 -1
- package/dist/agent/types.d.ts +2 -0
- package/dist/components/Application.d.ts +73 -18
- package/dist/components/Application.js +475 -64
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +1 -1
- package/dist/components/OptionsWatcher.js +65 -2
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -6
- package/dist/components/Scope.js +22 -6
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +5 -1
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +33 -7
- package/dist/components/componentSecrets.js +473 -59
- package/dist/components/componentSecrets.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +2 -2
- package/dist/components/deploymentRecorder.js +1 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/gitCredentialHelper.d.ts +1 -0
- package/dist/components/gitCredentialHelper.js +113 -0
- package/dist/components/gitCredentialHelper.js.map +1 -0
- package/dist/components/gitCredentialServer.d.ts +33 -0
- package/dist/components/gitCredentialServer.js +236 -0
- package/dist/components/gitCredentialServer.js.map +1 -0
- package/dist/components/mcp/tools/application.d.ts +19 -0
- package/dist/components/mcp/tools/application.js +180 -22
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +13 -0
- package/dist/components/mcp/tools/operations.js +1 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/derive.js +6 -2
- package/dist/components/mcp/tools/schemas/derive.js.map +1 -1
- package/dist/components/operations.js +23 -21
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +84 -21
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +33 -11
- package/dist/components/secretOperations.js +90 -26
- package/dist/components/secretOperations.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +21 -0
- package/dist/config/harperConfigEnvVars.js +95 -5
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/Resource.d.ts +19 -0
- package/dist/resources/Resource.js +93 -2
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/Table.d.ts +9 -0
- package/dist/resources/Table.js +256 -50
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +28 -25
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/defineResource.d.ts +180 -0
- package/dist/resources/defineResource.js +505 -0
- package/dist/resources/defineResource.js.map +1 -0
- package/dist/resources/defineTable.d.ts +221 -0
- package/dist/resources/defineTable.js +227 -0
- package/dist/resources/defineTable.js.map +1 -0
- package/dist/resources/jsonSchemaTypes.d.ts +2 -0
- package/dist/resources/jsonSchemaTypes.js +12 -4
- package/dist/resources/jsonSchemaTypes.js.map +1 -1
- package/dist/resources/openApi.js +69 -13
- package/dist/resources/openApi.js.map +1 -1
- package/dist/resources/search.js +6 -8
- package/dist/resources/search.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -2
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.js +35 -15
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +8 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +23 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +21 -0
- package/dist/utility/errors/hdbError.js +23 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/validation/configValidator.js +22 -6
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +33 -0
- package/npm-shrinkwrap.json +9197 -15236
- package/package.json +5 -3
- package/resources/DESIGN.md +42 -15
- package/resources/Resource.ts +99 -2
- package/resources/Table.ts +275 -71
- package/resources/analytics/read.ts +30 -25
- package/resources/defineResource.ts +651 -0
- package/resources/defineTable.ts +407 -0
- package/resources/jsonSchemaTypes.ts +12 -4
- package/resources/openApi.ts +68 -16
- package/resources/search.ts +5 -8
- package/server/graphqlQuerying.ts +4 -2
- package/server/http.ts +40 -16
- package/server/serverHelpers/serverUtilities.ts +19 -6
- package/server/serverHelpers/uwsServer.ts +24 -0
- package/studio/web/assets/{Chat-D4FIaBu7.js → Chat-CTjtL8Z4.js} +2 -2
- package/studio/web/assets/{Chat-D4FIaBu7.js.map → Chat-CTjtL8Z4.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-DGUC3L4r.js → FloatingChat-CafHR4Ur.js} +4 -4
- package/studio/web/assets/{FloatingChat-DGUC3L4r.js.map → FloatingChat-CafHR4Ur.js.map} +1 -1
- package/studio/web/assets/{applications-D8Am9ikM.js → applications-Buh_q0Vj.js} +2 -2
- package/studio/web/assets/{applications-D8Am9ikM.js.map → applications-Buh_q0Vj.js.map} +1 -1
- package/studio/web/assets/{index-HGL4WHdb.js → index-0hXeECkS.js} +6 -6
- package/studio/web/assets/{index-HGL4WHdb.js.map → index-0hXeECkS.js.map} +1 -1
- package/studio/web/assets/{index.lazy-Dk5j1AoQ.js → index.lazy-B00B7VBT.js} +4 -4
- package/studio/web/assets/{index.lazy-Dk5j1AoQ.js.map → index.lazy-B00B7VBT.js.map} +1 -1
- package/studio/web/assets/{profile-7bu_CF1f.js → profile-Cg2wwYPn.js} +2 -2
- package/studio/web/assets/{profile-7bu_CF1f.js.map → profile-Cg2wwYPn.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-MfYm9F94.js → setComponentFile-DCaDIvyB.js} +2 -2
- package/studio/web/assets/{setComponentFile-MfYm9F94.js.map → setComponentFile-DCaDIvyB.js.map} +1 -1
- package/studio/web/assets/{setup-SKA4UhdE.js → setup-CAVcAQjK.js} +2 -2
- package/studio/web/assets/{setup-SKA4UhdE.js.map → setup-CAVcAQjK.js.map} +1 -1
- package/studio/web/assets/{status-DetVLUxR.js → status-BRXorNdD.js} +2 -2
- package/studio/web/assets/{status-DetVLUxR.js.map → status-BRXorNdD.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-DLeYARY2.js → swagger-ui-react-Dy1D62vO.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-DLeYARY2.js.map → swagger-ui-react-Dy1D62vO.js.map} +1 -1
- package/studio/web/assets/{tsMode-DGO-jIF7.js → tsMode-A8gbL74v.js} +2 -2
- package/studio/web/assets/{tsMode-DGO-jIF7.js.map → tsMode-A8gbL74v.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-4M2i6bo4.js → useEntityRestURL-cDodrVcQ.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-4M2i6bo4.js.map → useEntityRestURL-cDodrVcQ.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +32 -0
- package/validation/configValidator.ts +23 -6
|
@@ -84,6 +84,12 @@ interface ResourceClassLike {
|
|
|
84
84
|
hidden?: boolean;
|
|
85
85
|
properties?: Record<string, unknown>;
|
|
86
86
|
outputSchemas?: { [verb: string]: object };
|
|
87
|
+
/** The URL path (may carry `:param`/`*wildcard` segments), used to bind path params for contract resources. */
|
|
88
|
+
path?: string;
|
|
89
|
+
/** Present when built via `Resource.withSchema` (RFC 0001, Pillar 2) — drives typed tool schemas + binding. */
|
|
90
|
+
requestContract?: unknown;
|
|
91
|
+
/** Per-verb `{ query?, body? }` JSON-Schema fragments derived from the request contract. */
|
|
92
|
+
inputSchemas?: { [verb: string]: { query?: JsonSchemaFragmentLike; body?: JsonSchemaFragmentLike } };
|
|
87
93
|
mcp?: { annotations?: { [verb: string]: ToolAnnotationsLike } };
|
|
88
94
|
get?: (target: unknown, request: unknown, data?: unknown) => unknown;
|
|
89
95
|
put?: (target: unknown, data: unknown, request: unknown) => unknown;
|
|
@@ -185,6 +191,119 @@ function isSimpleIdRoute(pattern: string): boolean {
|
|
|
185
191
|
return paramCount === 1;
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
/** A subset of `JsonSchemaFragment` (resources/jsonSchemaTypes.ts) — the shared IR request contracts reduce to. */
|
|
195
|
+
interface JsonSchemaFragmentLike {
|
|
196
|
+
type?: string | string[];
|
|
197
|
+
properties?: Record<string, unknown>;
|
|
198
|
+
required?: readonly string[];
|
|
199
|
+
items?: unknown;
|
|
200
|
+
enum?: readonly unknown[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** MCP verb → request-contract verb. `search` maps to `get`'s query grammar; others are 1:1. */
|
|
204
|
+
const CONTRACT_VERB: Record<string, 'get' | 'post' | 'put' | 'patch' | 'delete'> = {
|
|
205
|
+
get: 'get',
|
|
206
|
+
search: 'get',
|
|
207
|
+
create: 'post',
|
|
208
|
+
update: 'put',
|
|
209
|
+
patch: 'patch',
|
|
210
|
+
delete: 'delete',
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Parse `:param`/`*wildcard` segments out of a resource path (mirrors the route matcher). */
|
|
214
|
+
function pathParamNames(path?: string): string[] {
|
|
215
|
+
if (!path) return [];
|
|
216
|
+
return path
|
|
217
|
+
.split('/')
|
|
218
|
+
.filter((segment) => segment.charAt(0) === ':' || segment.charAt(0) === '*')
|
|
219
|
+
.map((segment) => (segment.charAt(0) === '*' ? segment.slice(1) || 'wildcard' : segment.slice(1)));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build an MCP tool input schema from a resource's request contract (RFC 0001, Pillar 2): the path
|
|
224
|
+
* params (always required strings) plus the verb's declared query and body fields, all off the shared
|
|
225
|
+
* JsonSchemaFragment. Returns undefined when there is nothing contract-derived to advertise (the caller
|
|
226
|
+
* then falls back to the generic table-attribute derivation).
|
|
227
|
+
*/
|
|
228
|
+
function contractInputSchema(ResourceClass: ResourceClassLike, contractVerb: string): object | undefined {
|
|
229
|
+
const input = ResourceClass.inputSchemas?.[contractVerb];
|
|
230
|
+
const params = pathParamNames(ResourceClass.path);
|
|
231
|
+
if (!input?.query?.properties && !input?.body?.properties && params.length === 0) return undefined;
|
|
232
|
+
const properties: Record<string, unknown> = {};
|
|
233
|
+
const required: string[] = [];
|
|
234
|
+
for (const param of params) {
|
|
235
|
+
properties[param] = { type: 'string', description: `Path parameter :${param}.` };
|
|
236
|
+
required.push(param);
|
|
237
|
+
}
|
|
238
|
+
const merge = (fragment?: JsonSchemaFragmentLike) => {
|
|
239
|
+
if (!fragment?.properties) return;
|
|
240
|
+
for (const [key, sub] of Object.entries(fragment.properties)) properties[key] = sub;
|
|
241
|
+
for (const key of fragment.required ?? []) if (!required.includes(key)) required.push(key);
|
|
242
|
+
};
|
|
243
|
+
merge(input?.query);
|
|
244
|
+
merge(input?.body);
|
|
245
|
+
const schema: { type: string; properties: Record<string, unknown>; required?: string[] } = {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties,
|
|
248
|
+
};
|
|
249
|
+
if (required.length) schema.required = required;
|
|
250
|
+
return schema;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Bind a contract resource's MCP arguments onto the RequestTarget the way the router would: path params
|
|
255
|
+
* as own properties (`target.id`, `target.n`, …) and declared query params via the URLSearchParams API
|
|
256
|
+
* (so `withSchema`'s pre-dispatch validation/coercion reads them). No-op for non-contract resources, so
|
|
257
|
+
* table tools are unaffected.
|
|
258
|
+
*/
|
|
259
|
+
function applyContractInputs(
|
|
260
|
+
target: Record<string, unknown>,
|
|
261
|
+
ResourceClass: ResourceClassLike,
|
|
262
|
+
a: Record<string, unknown>,
|
|
263
|
+
contractVerb: string
|
|
264
|
+
): void {
|
|
265
|
+
if (!ResourceClass.requestContract) return;
|
|
266
|
+
for (const param of pathParamNames(ResourceClass.path)) {
|
|
267
|
+
if (a[param] !== undefined) target[param] = a[param];
|
|
268
|
+
}
|
|
269
|
+
const queryProps = ResourceClass.inputSchemas?.[contractVerb]?.query?.properties;
|
|
270
|
+
const setter = (target as { set?: (n: string, v: string) => void }).set;
|
|
271
|
+
const appender = (target as { append?: (n: string, v: string) => void }).append;
|
|
272
|
+
if (queryProps && typeof setter === 'function') {
|
|
273
|
+
for (const key of Object.keys(queryProps)) {
|
|
274
|
+
const val = a[key];
|
|
275
|
+
if (val === undefined) continue;
|
|
276
|
+
if (Array.isArray(val) && typeof appender === 'function') {
|
|
277
|
+
for (const el of val) appender.call(target, key, String(el));
|
|
278
|
+
} else {
|
|
279
|
+
setter.call(target, key, String(val));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* MCP delivers path params, query params, and body fields flattened into ONE args object. For a
|
|
287
|
+
* contract resource those non-body keys are already bound onto the target by {@link applyContractInputs};
|
|
288
|
+
* strip them here so the remaining object is the request body the contract's `additionalProperties:false`
|
|
289
|
+
* body schema will accept (otherwise a bound `id`/query key poisons body validation). Non-contract
|
|
290
|
+
* resources pass through unchanged.
|
|
291
|
+
*/
|
|
292
|
+
function contractBody(
|
|
293
|
+
a: Record<string, unknown>,
|
|
294
|
+
ResourceClass: ResourceClassLike,
|
|
295
|
+
contractVerb: string
|
|
296
|
+
): Record<string, unknown> {
|
|
297
|
+
if (!ResourceClass.requestContract) return a;
|
|
298
|
+
const exclude = new Set(pathParamNames(ResourceClass.path));
|
|
299
|
+
const queryProps = ResourceClass.inputSchemas?.[contractVerb]?.query?.properties;
|
|
300
|
+
if (queryProps) for (const key of Object.keys(queryProps)) exclude.add(key);
|
|
301
|
+
if (exclude.size === 0) return a;
|
|
302
|
+
const body: Record<string, unknown> = {};
|
|
303
|
+
for (const [key, val] of Object.entries(a)) if (!exclude.has(key)) body[key] = val;
|
|
304
|
+
return body;
|
|
305
|
+
}
|
|
306
|
+
|
|
188
307
|
/** A compiled parameterised route (e.g. `/widget/:id`), stored outside the base Map. */
|
|
189
308
|
interface ParamRouteEntry {
|
|
190
309
|
pattern: string;
|
|
@@ -363,15 +482,40 @@ function wrapResult(data: unknown): ToolResult {
|
|
|
363
482
|
}
|
|
364
483
|
|
|
365
484
|
function wrapError(toolName: string, err: unknown): ToolResult {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
485
|
+
// `err` is arbitrary/untrusted — a revoked Proxy or a throwing getter would crash this
|
|
486
|
+
// error-serialization path if we accessed properties bare. Read everything inside one guard.
|
|
487
|
+
let message = `${toolName} failed`;
|
|
488
|
+
let errors: unknown;
|
|
489
|
+
let code: string | undefined;
|
|
490
|
+
let stack: string | undefined;
|
|
491
|
+
try {
|
|
492
|
+
const e = err as { message?: string; http_resp_msg?: string; errors?: unknown; code?: string; stack?: string };
|
|
493
|
+
if (e && typeof e === 'object') {
|
|
494
|
+
message = e.http_resp_msg ?? e.message ?? message;
|
|
495
|
+
errors = e.errors;
|
|
496
|
+
code = e.code;
|
|
497
|
+
stack = e.stack;
|
|
498
|
+
}
|
|
499
|
+
} catch (serializeErr) {
|
|
500
|
+
harperLogger.error(`Failed to read error in wrapError: ${(serializeErr as Error).message}`);
|
|
501
|
+
}
|
|
502
|
+
harperLogger.trace(`MCP ${toolName} threw: ${stack ?? message}`);
|
|
503
|
+
const payload: { kind: string; tool: string; message: string; code?: string; errors?: unknown } = {
|
|
504
|
+
kind: 'harper_error',
|
|
505
|
+
tool: toolName,
|
|
506
|
+
message,
|
|
507
|
+
};
|
|
508
|
+
// Pass structured validation issues through rather than flattening to a single string (RFC 0001 §8).
|
|
509
|
+
if (Array.isArray(errors) && errors.length) {
|
|
510
|
+
payload.code = code;
|
|
511
|
+
payload.errors = errors;
|
|
512
|
+
}
|
|
369
513
|
return {
|
|
370
514
|
isError: true,
|
|
371
515
|
content: [
|
|
372
516
|
{
|
|
373
517
|
type: 'text',
|
|
374
|
-
text: JSON.stringify(
|
|
518
|
+
text: JSON.stringify(payload),
|
|
375
519
|
},
|
|
376
520
|
],
|
|
377
521
|
};
|
|
@@ -387,6 +531,7 @@ function makeGetHandler(toolName: string, path: string, capturedClass: ResourceC
|
|
|
387
531
|
const target = makeTarget();
|
|
388
532
|
target.id = a.id;
|
|
389
533
|
if (Array.isArray(a.get_attributes)) target.select = a.get_attributes as string[];
|
|
534
|
+
applyContractInputs(target, ResourceClass, a, 'get');
|
|
390
535
|
const data = await ResourceClass.get!(target, buildContext(context.user));
|
|
391
536
|
return wrapResult(data);
|
|
392
537
|
} catch (err) {
|
|
@@ -455,7 +600,12 @@ function makeCreateHandler(toolName: string, path: string, capturedClass: Resour
|
|
|
455
600
|
// base `post` throws `missingMethod` ("does not have a post method")
|
|
456
601
|
// because a record-scoped resource has no insert path (#1317).
|
|
457
602
|
target.isCollection = true;
|
|
458
|
-
|
|
603
|
+
applyContractInputs(target, ResourceClass, a, 'post');
|
|
604
|
+
const data = await ResourceClass.post!(
|
|
605
|
+
target,
|
|
606
|
+
contractBody(a, ResourceClass, 'post'),
|
|
607
|
+
buildContext(context.user)
|
|
608
|
+
);
|
|
459
609
|
// Standard table create resolves to the new record's primary key (a
|
|
460
610
|
// scalar). Wrap it as `{ id }` so the result carries `structuredContent`
|
|
461
611
|
// matching `deriveCreateOutputSchema`; strict SDK clients reject a bare
|
|
@@ -477,13 +627,17 @@ function makeUpdateHandler(toolName: string, path: string, capturedClass: Resour
|
|
|
477
627
|
const ResourceClass = liveResource(path, capturedClass);
|
|
478
628
|
const target = makeTarget();
|
|
479
629
|
target.id = id;
|
|
630
|
+
applyContractInputs(target, ResourceClass, a, verb);
|
|
480
631
|
// Call the verb method *on* ResourceClass so `this` stays bound to the
|
|
481
632
|
// class — detaching it (`const fn = ResourceClass.put`) makes the static
|
|
482
633
|
// Resource dispatcher read `this.directURLMapping` off undefined and throw.
|
|
483
634
|
const ctx = buildContext(context.user);
|
|
635
|
+
// For a contract resource, strip path/query keys from the body (they're bound onto the target
|
|
636
|
+
// above); otherwise keep the historical `id`-stripped rest.
|
|
637
|
+
const body = ResourceClass.requestContract ? contractBody(a, ResourceClass, verb) : rest;
|
|
484
638
|
const data = await (verb === 'put'
|
|
485
|
-
? ResourceClass.put!(target,
|
|
486
|
-
: ResourceClass.patch!(target,
|
|
639
|
+
? ResourceClass.put!(target, body, ctx)
|
|
640
|
+
: ResourceClass.patch!(target, body, ctx));
|
|
487
641
|
// Table.put/patch resolve to undefined; surface a `{ ok: true }`
|
|
488
642
|
// acknowledgement so the result has structuredContent matching
|
|
489
643
|
// derive{Update,Patch}OutputSchema. A custom Resource that returns a
|
|
@@ -503,6 +657,7 @@ function makeDeleteHandler(toolName: string, path: string, capturedClass: Resour
|
|
|
503
657
|
const ResourceClass = liveResource(path, capturedClass);
|
|
504
658
|
const target = makeTarget();
|
|
505
659
|
target.id = a.id;
|
|
660
|
+
applyContractInputs(target, ResourceClass, a, 'delete');
|
|
506
661
|
const data = await ResourceClass.delete!(target, buildContext(context.user));
|
|
507
662
|
// Table.delete resolves to a boolean; wrap it as `{ deleted }` so the
|
|
508
663
|
// result carries structuredContent matching deriveDeleteOutputSchema. A
|
|
@@ -670,14 +825,26 @@ function registerVerbTools(ctx: ResourceContext): number {
|
|
|
670
825
|
const primaryKey = ResourceClass.primaryKey;
|
|
671
826
|
const ctxForVerb: VerbDescriptionContext = { tableDoc, tableName, primaryKey, path };
|
|
672
827
|
|
|
673
|
-
|
|
828
|
+
// `outputSchemas` is authored/keyed by MCP verb (`create`/`update`) for table resources, but a request
|
|
829
|
+
// contract stores it by HTTP verb (`post`/`put`); fall back to the HTTP-verb key so a contract's
|
|
830
|
+
// `post.response`/`put.response` reaches the `create_*`/`update_*` output schema.
|
|
831
|
+
const overrideOutput = (verb: Verb): object | undefined =>
|
|
832
|
+
ResourceClass.outputSchemas?.[verb] ?? ResourceClass.outputSchemas?.[CONTRACT_VERB[verb]];
|
|
833
|
+
// When the resource carries a request contract (RFC 0001, Pillar 2), drive the tool INPUT schema off
|
|
834
|
+
// the contract (path params + declared query/body) instead of the generic table-attribute derivation.
|
|
835
|
+
// `search_*` is excluded: the contract has no `search` verb and the `search`/`query` static isn't
|
|
836
|
+
// wrapped by the contract validator, so it keeps the rich generic conditions/sort/limit grammar.
|
|
837
|
+
const overrideInput = (verb: Verb): object | undefined =>
|
|
838
|
+
ResourceClass.requestContract && verb !== 'search'
|
|
839
|
+
? contractInputSchema(ResourceClass, CONTRACT_VERB[verb])
|
|
840
|
+
: undefined;
|
|
674
841
|
|
|
675
842
|
if (verbs.get) {
|
|
676
843
|
const name = `get_${suffix}`;
|
|
677
844
|
addTool({
|
|
678
845
|
name,
|
|
679
846
|
description: verbDescription('get', ctxForVerb),
|
|
680
|
-
inputSchema: deriveGetSchema(attributes, undefined),
|
|
847
|
+
inputSchema: overrideInput('get') ?? deriveGetSchema(attributes, undefined),
|
|
681
848
|
outputSchema: overrideOutput('get') ?? deriveGetOutputSchema(attributes, undefined),
|
|
682
849
|
profile: 'application',
|
|
683
850
|
annotations: mergeAnnotations('get', { readOnlyHint: true }, ResourceClass),
|
|
@@ -693,7 +860,7 @@ function registerVerbTools(ctx: ResourceContext): number {
|
|
|
693
860
|
addTool({
|
|
694
861
|
name,
|
|
695
862
|
description: verbDescription('search', ctxForVerb),
|
|
696
|
-
inputSchema: deriveSearchSchema(attributes, undefined),
|
|
863
|
+
inputSchema: overrideInput('search') ?? deriveSearchSchema(attributes, undefined),
|
|
697
864
|
profile: 'application',
|
|
698
865
|
annotations: mergeAnnotations('search', { readOnlyHint: true }, ResourceClass),
|
|
699
866
|
visibleTo: makeVisibleTo(databaseName, tableName, 'read'),
|
|
@@ -706,7 +873,7 @@ function registerVerbTools(ctx: ResourceContext): number {
|
|
|
706
873
|
addTool({
|
|
707
874
|
name,
|
|
708
875
|
description: verbDescription('create', ctxForVerb),
|
|
709
|
-
inputSchema: deriveCreateSchema(attributes, undefined),
|
|
876
|
+
inputSchema: overrideInput('create') ?? deriveCreateSchema(attributes, undefined),
|
|
710
877
|
outputSchema: overrideOutput('create') ?? deriveCreateOutputSchema(attributes, undefined),
|
|
711
878
|
profile: 'application',
|
|
712
879
|
annotations: mergeAnnotations('create', {}, ResourceClass),
|
|
@@ -720,7 +887,7 @@ function registerVerbTools(ctx: ResourceContext): number {
|
|
|
720
887
|
addTool({
|
|
721
888
|
name,
|
|
722
889
|
description: verbDescription('update', ctxForVerb),
|
|
723
|
-
inputSchema: deriveUpdateSchema(attributes, undefined),
|
|
890
|
+
inputSchema: overrideInput('update') ?? deriveUpdateSchema(attributes, undefined),
|
|
724
891
|
outputSchema: overrideOutput('update') ?? deriveUpdateOutputSchema(attributes, undefined),
|
|
725
892
|
profile: 'application',
|
|
726
893
|
// PUT semantics: replacing with the same payload yields the same state,
|
|
@@ -735,7 +902,7 @@ function registerVerbTools(ctx: ResourceContext): number {
|
|
|
735
902
|
addTool({
|
|
736
903
|
name,
|
|
737
904
|
description: verbDescription('patch', ctxForVerb),
|
|
738
|
-
inputSchema: deriveUpdateSchema(attributes, undefined),
|
|
905
|
+
inputSchema: overrideInput('patch') ?? deriveUpdateSchema(attributes, undefined),
|
|
739
906
|
outputSchema: overrideOutput('patch') ?? derivePatchOutputSchema(attributes, undefined),
|
|
740
907
|
profile: 'application',
|
|
741
908
|
// patch_* idempotency depends on partial-update semantics; default
|
|
@@ -755,7 +922,7 @@ function registerVerbTools(ctx: ResourceContext): number {
|
|
|
755
922
|
addTool({
|
|
756
923
|
name,
|
|
757
924
|
description: verbDescription('delete', ctxForVerb),
|
|
758
|
-
inputSchema: deriveDeleteSchema(attributes, undefined),
|
|
925
|
+
inputSchema: overrideInput('delete') ?? deriveDeleteSchema(attributes, undefined),
|
|
759
926
|
outputSchema: overrideOutput('delete') ?? deriveDeleteOutputSchema(attributes),
|
|
760
927
|
profile: 'application',
|
|
761
928
|
// delete_* idempotency depends on delete-of-deleted behavior; default
|
|
@@ -1148,6 +1315,9 @@ function buildApplicationTools(resources: ResourcesRegistry): void {
|
|
|
1148
1315
|
// segments yet, so ALL generated verbs are dropped. Author-defined
|
|
1149
1316
|
// mcpTools/mcpPrompts carry their own schemas and handler methods, so they
|
|
1150
1317
|
// register regardless of binding mode.
|
|
1318
|
+
// A request contract (RFC 0001, Pillar 2) LIFTS this: `applyContractInputs` binds arbitrary
|
|
1319
|
+
// path params + declared query itself, so contract resources register their generated verbs on
|
|
1320
|
+
// any route shape — this is the "richer binding rides on the contract" the plain paths defer.
|
|
1151
1321
|
const considerEntry = (
|
|
1152
1322
|
path: string,
|
|
1153
1323
|
entry: ResourceRegistryEntry | undefined,
|
|
@@ -1164,14 +1334,18 @@ function buildApplicationTools(resources: ResourcesRegistry): void {
|
|
|
1164
1334
|
return;
|
|
1165
1335
|
}
|
|
1166
1336
|
const verbs = detectVerbs(ResourceClass);
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
)
|
|
1174
|
-
|
|
1337
|
+
// A request contract binds arbitrary path params + query (applyContractInputs), so it is exempt
|
|
1338
|
+
// from the generated-handler binding restrictions below.
|
|
1339
|
+
if (!ResourceClass.requestContract) {
|
|
1340
|
+
if (paramBinding === 'id') {
|
|
1341
|
+
verbs.search = false;
|
|
1342
|
+
verbs.create = false;
|
|
1343
|
+
} else if (paramBinding === 'none') {
|
|
1344
|
+
harperLogger.trace(
|
|
1345
|
+
`MCP application: '/${path}' generated verb tools skipped — multi-segment/named-wildcard binding not yet supported`
|
|
1346
|
+
);
|
|
1347
|
+
verbs.get = verbs.search = verbs.create = verbs.updatePut = verbs.updatePatch = verbs.delete = false;
|
|
1348
|
+
}
|
|
1175
1349
|
}
|
|
1176
1350
|
const hasVerbs = verbs.get || verbs.search || verbs.create || verbs.updatePut || verbs.updatePatch || verbs.delete;
|
|
1177
1351
|
const hasCustomTools = Array.isArray(ResourceClass?.mcpTools) && ResourceClass.mcpTools.length > 0;
|
|
@@ -1207,7 +1381,7 @@ function buildApplicationTools(resources: ResourcesRegistry): void {
|
|
|
1207
1381
|
// tools — even though it appears in the OpenAPI document, which already iterates `paramRoutes`.
|
|
1208
1382
|
// Enumerate them so the tool surface matches the REST surface; the binding mode restricts the
|
|
1209
1383
|
// GENERATED verb tools to what their handlers actually bind (see considerEntry), while custom
|
|
1210
|
-
// mcpTools/mcpPrompts register on every route shape.
|
|
1384
|
+
// mcpTools/mcpPrompts (and contract resources) register on every route shape.
|
|
1211
1385
|
for (const route of resources.paramRoutes ?? []) {
|
|
1212
1386
|
considerEntry(route.pattern, route.entry, isSimpleIdRoute(route.pattern) ? 'id' : 'none');
|
|
1213
1387
|
}
|
|
@@ -276,7 +276,7 @@ function buildDescription(operationName: string, hasCuratedSchema: boolean): str
|
|
|
276
276
|
* matches the MCP spec's `tools/call` convention so the LLM sees and can
|
|
277
277
|
* adapt to the failure.
|
|
278
278
|
*/
|
|
279
|
-
function makeOperationToolHandler(operationName: string) {
|
|
279
|
+
export function makeOperationToolHandler(operationName: string) {
|
|
280
280
|
return async function operationToolHandler(args: unknown, context: { user: AuthedUser }): Promise<ToolResult> {
|
|
281
281
|
const body: Record<string, unknown> = {
|
|
282
282
|
...(args && typeof args === 'object' ? (args as Record<string, unknown>) : {}),
|
|
@@ -70,12 +70,16 @@ function harperTypeToJsonSchema(type: string | undefined): { type: string | stri
|
|
|
70
70
|
|
|
71
71
|
function attributeToProperty(attr: HarperAttribute): object {
|
|
72
72
|
let base: { type?: string | string[]; description?: string; [key: string]: unknown };
|
|
73
|
-
|
|
73
|
+
// The GraphQL parser emits nested objects via `.properties` (not a capitalized `'Object'` type) and
|
|
74
|
+
// list types as lowercase `type: 'array'` with `.elements` — the prior `'Object'`/`'Array'` literal
|
|
75
|
+
// checks never matched, so nested shapes fell through to a bare `{ type: 'string' }`. Detect them the
|
|
76
|
+
// way the parser actually emits, matching the shared `attributeToFragment` projector.
|
|
77
|
+
if (attr.properties) {
|
|
74
78
|
base = {
|
|
75
79
|
type: 'object',
|
|
76
80
|
properties: Object.fromEntries(attr.properties.map((p) => [p.name, attributeToProperty(p)])),
|
|
77
81
|
};
|
|
78
|
-
} else if (attr.type === '
|
|
82
|
+
} else if (attr.type === 'array' && attr.elements) {
|
|
79
83
|
base = {
|
|
80
84
|
type: 'array',
|
|
81
85
|
items: attributeToProperty(attr.elements),
|
package/components/operations.js
CHANGED
|
@@ -370,15 +370,15 @@ async function deployComponent(req) {
|
|
|
370
370
|
throw handleHDBError(validation, validation.message, HTTP_STATUS_CODES.BAD_REQUEST);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// Ingest any provided
|
|
373
|
+
// Ingest any provided credential token into the secrets store so the credential lives as
|
|
374
374
|
// replicated ciphertext (reference, not embed); already-reference entries pass through, and with
|
|
375
375
|
// no custody a literal token stays as a transient, this-node-only fallback (#1158). Peers
|
|
376
376
|
// re-running a replicated deploy already carry references and never re-ingest.
|
|
377
|
-
const {
|
|
378
|
-
req.
|
|
377
|
+
const { ingestCredentials, resolveCredentials } = require('./secretOperations.ts');
|
|
378
|
+
req.credentials = await ingestCredentials(req, req.credentials, req.project);
|
|
379
379
|
// References are safe to persist (config + deployment row) and replicate; a no-custody literal
|
|
380
380
|
// token is not — it is used only for this node's install below, then stripped before replication.
|
|
381
|
-
const
|
|
381
|
+
const credentialReferences = (req.credentials ?? []).filter((entry) => entry && entry.secret !== undefined);
|
|
382
382
|
|
|
383
383
|
// Write to root config if the request contains a package identifier
|
|
384
384
|
if (req.package) {
|
|
@@ -403,9 +403,9 @@ async function deployComponent(req) {
|
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
405
|
if (req.urlPath !== undefined) applicationConfig.urlPath = req.urlPath;
|
|
406
|
-
// Persist
|
|
406
|
+
// Persist credential references (never tokens) so every cold install of this component —
|
|
407
407
|
// reboot, new peer, rollback — re-resolves the credential from the store.
|
|
408
|
-
if (
|
|
408
|
+
if (credentialReferences.length) applicationConfig.credentials = credentialReferences;
|
|
409
409
|
await configUtils.addConfig(req.project, applicationConfig);
|
|
410
410
|
}
|
|
411
411
|
|
|
@@ -430,8 +430,8 @@ async function deployComponent(req) {
|
|
|
430
430
|
package_identifier: req.package ?? null,
|
|
431
431
|
user: req.hdb_user?.username,
|
|
432
432
|
restart_mode: req.restart === 'rolling' ? 'rolling' : req.restart ? 'immediate' : null,
|
|
433
|
-
// Reference form only — the rollback source for re-resolving
|
|
434
|
-
|
|
433
|
+
// Reference form only — the rollback source for re-resolving the credential.
|
|
434
|
+
credentials: credentialReferences.length ? credentialReferences : null,
|
|
435
435
|
emitter,
|
|
436
436
|
});
|
|
437
437
|
if (recorder) req._deploymentId = recorder.deploymentId;
|
|
@@ -469,17 +469,17 @@ async function deployComponent(req) {
|
|
|
469
469
|
extractionPayload = row.payload_blob.stream();
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
// Resolve
|
|
472
|
+
// Resolve credential references into concrete tokens for this node's npm pack/install
|
|
473
473
|
// (a no-custody literal-token fallback passes through unchanged). On a peer running a
|
|
474
474
|
// replicated deploy, the referenced hdb_secret row may arrive just behind the deploy op, so
|
|
475
475
|
// allow a bounded grace period (same budget as the payload-row wait) for it to replicate in.
|
|
476
|
-
let
|
|
476
|
+
let credentialsWaitMs = 0;
|
|
477
477
|
if (isReplicatedExecution) {
|
|
478
478
|
const requested = Number(req.deployment_timeout);
|
|
479
|
-
|
|
479
|
+
credentialsWaitMs = Number.isFinite(requested) && requested >= 0 ? requested : DEFAULT_AWAIT_ROW_TIMEOUT_MS;
|
|
480
480
|
}
|
|
481
|
-
const
|
|
482
|
-
waitMs:
|
|
481
|
+
const resolvedCredentials = await resolveCredentials(req.credentials, req.project, {
|
|
482
|
+
waitMs: credentialsWaitMs,
|
|
483
483
|
});
|
|
484
484
|
|
|
485
485
|
const application = new Application({
|
|
@@ -498,16 +498,18 @@ async function deployComponent(req) {
|
|
|
498
498
|
installCapture.push(manager, stream, line);
|
|
499
499
|
if (emitter) emit('install', { manager, stream, line });
|
|
500
500
|
},
|
|
501
|
-
//
|
|
502
|
-
|
|
501
|
+
// Deploy credentials (already resolved above), used here for this node's npm pack/install:
|
|
502
|
+
// registry entries via a transient .npmrc, git-host entries via the in-memory credential
|
|
503
|
+
// socket the clone spawn talks to.
|
|
504
|
+
credentials: resolvedCredentials,
|
|
503
505
|
});
|
|
504
|
-
// Reduce req.
|
|
506
|
+
// Reduce req.credentials to references only (never a token) before it can reach an error/log
|
|
505
507
|
// path or replication: references are what peers resolve from their own replicated hdb_secret
|
|
506
508
|
// copy; a no-custody literal token is dropped entirely (peers fall back to their fabric-injected
|
|
507
509
|
// NPM_CONFIG_USERCONFIG, as before). This also fixes the prior success-only strip that leaked a
|
|
508
510
|
// literal token on a prepare/load failure.
|
|
509
|
-
if (
|
|
510
|
-
else delete req.
|
|
511
|
+
if (credentialReferences.length) req.credentials = credentialReferences;
|
|
512
|
+
else delete req.credentials;
|
|
511
513
|
|
|
512
514
|
emit('phase', { phase: 'prepare', status: 'start' });
|
|
513
515
|
await prepareApplication(application);
|
|
@@ -556,7 +558,7 @@ async function deployComponent(req) {
|
|
|
556
558
|
// ProgressEmitter holds function listeners that can't survive the replication
|
|
557
559
|
// channel's serialization; strip it unconditionally.
|
|
558
560
|
delete req.progress;
|
|
559
|
-
// req.
|
|
561
|
+
// req.credentials was already deleted immediately after the Application ctor (above) so the
|
|
560
562
|
// token never reaches the replication channel or a peer's operation log; peers authenticate
|
|
561
563
|
// against the private registry via their own fabric-injected NPM_CONFIG_USERCONFIG on reinstall.
|
|
562
564
|
if (systemReplicated && recorder) {
|
|
@@ -371,6 +371,69 @@ function packageComponentValidator(req) {
|
|
|
371
371
|
return validator.validateBySchema(req, packageProjSchema);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
// An npm registry-auth credential entry, identified by its `registry` key. `host` is forbidden
|
|
375
|
+
// rather than merely unused: operation validation allows unknown keys, so without this an entry
|
|
376
|
+
// carrying both discriminators would validate as npm registry auth and its git half would be
|
|
377
|
+
// silently dropped.
|
|
378
|
+
const REGISTRY_CREDENTIAL_ENTRY = Joi.object({
|
|
379
|
+
host: Joi.any().forbidden().messages({
|
|
380
|
+
'any.unknown': `a credential entry is either npm registry auth ('registry') or git host auth ('host'), not both`,
|
|
381
|
+
}),
|
|
382
|
+
// registry and token are written verbatim into the transient .npmrc, which is line-based;
|
|
383
|
+
// forbid CR/LF so a super_user can't inject extra npm config lines. (registry also accepts
|
|
384
|
+
// bare hosts and //host/ forms, so a strict URI validator would reject supported inputs — the
|
|
385
|
+
// newline guard is the right scope here.)
|
|
386
|
+
registry: Joi.string()
|
|
387
|
+
.pattern(/^[^\r\n]+$/)
|
|
388
|
+
.required(),
|
|
389
|
+
token: Joi.string().pattern(/^[^\r\n]+$/),
|
|
390
|
+
// A reference into the hdb_secret store; same name grammar as set_secret's `name`.
|
|
391
|
+
secret: Joi.string()
|
|
392
|
+
.pattern(ENV_KEY_REGEX)
|
|
393
|
+
.messages({ 'string.pattern.base': `'secret' must only contain word characters, dots and dashes` }),
|
|
394
|
+
scope: Joi.string()
|
|
395
|
+
.pattern(/^@[a-z0-9-_.]+$/)
|
|
396
|
+
.optional(),
|
|
397
|
+
})
|
|
398
|
+
.xor('token', 'secret')
|
|
399
|
+
// The whole operation validates with allowUnknown, but a credential entry must not: an unknown
|
|
400
|
+
// key here (a typo'd or future secret-bearing field like `password`) would pass through ingest
|
|
401
|
+
// unchanged and be persisted to config/hdb_deployment and replicated, defeating reference-only.
|
|
402
|
+
.unknown(false);
|
|
403
|
+
|
|
404
|
+
// A git-host credential entry, identified by its `host` key (#1792). Used to authenticate the
|
|
405
|
+
// `git clone`/`git ls-remote` npm runs for a git-reference `package` (e.g. `github:org/repo`); the
|
|
406
|
+
// token is served to git from memory, never written to a file or a URL.
|
|
407
|
+
const GIT_CREDENTIAL_ENTRY = Joi.object({
|
|
408
|
+
// A bare host, optionally with a port — `github.com`, `git.example.com:8443`. A scheme or path is
|
|
409
|
+
// tolerated and normalized away, but a credential is matched by host, so keep the grammar tight.
|
|
410
|
+
host: Joi.string()
|
|
411
|
+
.pattern(/^[^\s/@\\]+$/)
|
|
412
|
+
.required()
|
|
413
|
+
.messages({ 'string.pattern.base': `'host' must be a bare git host, e.g. 'github.com'` }),
|
|
414
|
+
// The username half of git's HTTPS basic auth. Defaults to GitHub's `x-access-token` convention;
|
|
415
|
+
// GitLab wants `oauth2` and Bitbucket `x-token-auth`.
|
|
416
|
+
username: Joi.string()
|
|
417
|
+
.pattern(/^[^\r\n:]+$/)
|
|
418
|
+
.optional(),
|
|
419
|
+
// Capped like other secret-bearing fields (SECRET_MAX_LENGTH, above): an unbounded literal token
|
|
420
|
+
// here feeds straight into synchronous envelope-sealing crypto, so without a cap it's a
|
|
421
|
+
// resource-exhaustion vector, not just a storage one.
|
|
422
|
+
token: Joi.string()
|
|
423
|
+
.pattern(/^[^\r\n]+$/)
|
|
424
|
+
.max(SECRET_MAX_LENGTH),
|
|
425
|
+
secret: Joi.string()
|
|
426
|
+
.pattern(ENV_KEY_REGEX)
|
|
427
|
+
.messages({ 'string.pattern.base': `'secret' must only contain word characters, dots and dashes` }),
|
|
428
|
+
// `registry` forbidden for symmetry with the npm entry: an entry carrying both discriminators has
|
|
429
|
+
// no single kind and must be rejected, not silently treated as git auth.
|
|
430
|
+
registry: Joi.any().forbidden().messages({
|
|
431
|
+
'any.unknown': `a credential entry is either npm registry auth ('registry') or git host auth ('host'), not both`,
|
|
432
|
+
}),
|
|
433
|
+
})
|
|
434
|
+
.xor('token', 'secret')
|
|
435
|
+
.unknown(false);
|
|
436
|
+
|
|
374
437
|
/**
|
|
375
438
|
* Validate deployComponent requests.
|
|
376
439
|
* @param req
|
|
@@ -398,33 +461,35 @@ function deployComponentValidator(req) {
|
|
|
398
461
|
})
|
|
399
462
|
.optional()
|
|
400
463
|
.messages({ 'any.invalid': 'urlPath must not contain ".."' }),
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
464
|
+
// Deploy credentials. The array is kind-heterogeneous: an entry's kind is implied by its
|
|
465
|
+
// identifying key rather than a separate discriminator field, so a new kind is added as
|
|
466
|
+
// another item alternative here without reshaping the field. Today: npm registry auth
|
|
467
|
+
// (`registry`) and git host auth for a git-reference package (`host`, #1792).
|
|
468
|
+
//
|
|
469
|
+
// Every kind supplies its credential exactly one of two ways:
|
|
470
|
+
// - `token`: a literal token, used only for this node's install and never persisted or
|
|
471
|
+
// replicated (stripped from req before replicateOperation).
|
|
404
472
|
// - `secret`: the name of an hdb_secret row (#1550); the token is resolved by decrypting
|
|
405
473
|
// that row on this node at deploy time, so the credential lives in the secrets store
|
|
406
474
|
// (reference, not embed) instead of travelling in the operation body.
|
|
407
|
-
|
|
475
|
+
// Dispatched on the presence of `registry` rather than tried as alternatives, so a malformed
|
|
476
|
+
// entry reports what is actually wrong with it (a newline in the token, an invalid scope) rather
|
|
477
|
+
// than a generic "no alternative matched".
|
|
478
|
+
credentials: Joi.array()
|
|
408
479
|
.items(
|
|
409
|
-
Joi.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
registry: Joi.string()
|
|
415
|
-
.pattern(/^[^\r\n]+$/)
|
|
416
|
-
.required(),
|
|
417
|
-
token: Joi.string().pattern(/^[^\r\n]+$/),
|
|
418
|
-
// A reference into the hdb_secret store; same name grammar as set_secret's `name`.
|
|
419
|
-
secret: Joi.string()
|
|
420
|
-
.pattern(ENV_KEY_REGEX)
|
|
421
|
-
.messages({ 'string.pattern.base': `'secret' must only contain word characters, dots and dashes` }),
|
|
422
|
-
scope: Joi.string()
|
|
423
|
-
.pattern(/^@[a-z0-9-_.]+$/)
|
|
424
|
-
.optional(),
|
|
425
|
-
}).xor('token', 'secret')
|
|
480
|
+
Joi.alternatives().conditional('.registry', {
|
|
481
|
+
is: Joi.exist(),
|
|
482
|
+
then: REGISTRY_CREDENTIAL_ENTRY,
|
|
483
|
+
otherwise: GIT_CREDENTIAL_ENTRY,
|
|
484
|
+
})
|
|
426
485
|
)
|
|
427
486
|
.optional(),
|
|
487
|
+
// `registryAuth` was this field's name on the 5.2 dev line before it grew to carry other
|
|
488
|
+
// credential kinds. Rejected rather than ignored: validation allows unknown keys, so a caller
|
|
489
|
+
// still sending it would otherwise get a deploy that silently installs with no credentials.
|
|
490
|
+
registryAuth: Joi.any().forbidden().messages({
|
|
491
|
+
'any.unknown': `'registryAuth' has been renamed to 'credentials'`,
|
|
492
|
+
}),
|
|
428
493
|
}).with('urlPath', 'package');
|
|
429
494
|
|
|
430
495
|
return validator.validateBySchema(req, deployProjSchema);
|