@vectros-ai/cli 0.6.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +7 -0
- package/dist/cli.js +1339 -95
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1339 -95
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -675,6 +675,10 @@ var ReadAccessLogResponse;
|
|
|
675
675
|
})(ReadAccessLogResponse || (ReadAccessLogResponse = {}));
|
|
676
676
|
var RecordRequest;
|
|
677
677
|
((RecordRequest2) => {
|
|
678
|
+
RecordRequest2.Status = {
|
|
679
|
+
Active: "ACTIVE",
|
|
680
|
+
Archived: "ARCHIVED"
|
|
681
|
+
};
|
|
678
682
|
RecordRequest2.IndexMode = {
|
|
679
683
|
Hybrid: "HYBRID",
|
|
680
684
|
Semantic: "SEMANTIC",
|
|
@@ -684,6 +688,10 @@ var RecordRequest;
|
|
|
684
688
|
})(RecordRequest || (RecordRequest = {}));
|
|
685
689
|
var RecordResponse;
|
|
686
690
|
((RecordResponse2) => {
|
|
691
|
+
RecordResponse2.Status = {
|
|
692
|
+
Active: "ACTIVE",
|
|
693
|
+
Archived: "ARCHIVED"
|
|
694
|
+
};
|
|
687
695
|
RecordResponse2.IndexStatus = {
|
|
688
696
|
PendingIndex: "PENDING_INDEX",
|
|
689
697
|
Indexed: "INDEXED",
|
|
@@ -2306,7 +2314,7 @@ function withNoOpAuthProvider(options) {
|
|
|
2306
2314
|
authProvider: new NoOpAuthProvider()
|
|
2307
2315
|
};
|
|
2308
2316
|
}
|
|
2309
|
-
function handleNonStatusCodeError(error, rawResponse, method,
|
|
2317
|
+
function handleNonStatusCodeError(error, rawResponse, method, path5) {
|
|
2310
2318
|
switch (error.reason) {
|
|
2311
2319
|
case "non-json":
|
|
2312
2320
|
throw new VectrosError({
|
|
@@ -2320,7 +2328,7 @@ function handleNonStatusCodeError(error, rawResponse, method, path4) {
|
|
|
2320
2328
|
rawResponse
|
|
2321
2329
|
});
|
|
2322
2330
|
case "timeout":
|
|
2323
|
-
throw new VectrosTimeoutError(`Timeout exceeded when calling ${method} ${
|
|
2331
|
+
throw new VectrosTimeoutError(`Timeout exceeded when calling ${method} ${path5}.`, {
|
|
2324
2332
|
cause: error.cause
|
|
2325
2333
|
});
|
|
2326
2334
|
case "unknown":
|
|
@@ -3158,7 +3166,7 @@ var AuthClient = class {
|
|
|
3158
3166
|
* roleId: "engineering-member",
|
|
3159
3167
|
* name: "Engineering Team Member",
|
|
3160
3168
|
* scopes: [{
|
|
3161
|
-
* allowed_actions: ["
|
|
3169
|
+
* allowed_actions: ["records:cru", "search:r"]
|
|
3162
3170
|
* }]
|
|
3163
3171
|
* }
|
|
3164
3172
|
* })
|
|
@@ -3706,7 +3714,7 @@ var AuthClient = class {
|
|
|
3706
3714
|
* roleId: "engineering-member",
|
|
3707
3715
|
* name: "Engineering Team Member",
|
|
3708
3716
|
* scopes: [{
|
|
3709
|
-
* allowed_actions: ["
|
|
3717
|
+
* allowed_actions: ["records:cru", "search:r"]
|
|
3710
3718
|
* }]
|
|
3711
3719
|
* }
|
|
3712
3720
|
* })
|
|
@@ -4609,7 +4617,7 @@ var DocumentsClient = class {
|
|
|
4609
4617
|
this._options = normalizeClientOptionsWithAuth(options);
|
|
4610
4618
|
}
|
|
4611
4619
|
/**
|
|
4612
|
-
* Returns a paginated list of your documents, optionally filtered by folder (`folderId`) and/or owner (`userId`, `orgId`, or `
|
|
4620
|
+
* Returns a paginated list of your documents, optionally filtered by folder (`folderId`) and/or owner (`userId`, `orgId`, `clientId`, or `scope`). The response is a `{data, nextCursor}` envelope; pass `nextCursor` back as `startFrom` to fetch the next page. Requires the `documents:r` scope.
|
|
4613
4621
|
*
|
|
4614
4622
|
* @param {Vectros.ListDocumentsRequest} request
|
|
4615
4623
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4619,6 +4627,7 @@ var DocumentsClient = class {
|
|
|
4619
4627
|
* userId: "550e8400-e29b-41d4-a716-446655440000",
|
|
4620
4628
|
* orgId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
4621
4629
|
* clientId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
4630
|
+
* scope: "group:eng-team",
|
|
4622
4631
|
* folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
4623
4632
|
* startFrom: "doc_prev123"
|
|
4624
4633
|
* })
|
|
@@ -4627,11 +4636,12 @@ var DocumentsClient = class {
|
|
|
4627
4636
|
return HttpResponsePromise.fromPromise(this.__listDocuments(request, requestOptions));
|
|
4628
4637
|
}
|
|
4629
4638
|
async __listDocuments(request = {}, requestOptions) {
|
|
4630
|
-
const { userId, orgId, clientId, folderId, startFrom, limit } = request;
|
|
4639
|
+
const { userId, orgId, clientId, scope, folderId, startFrom, limit } = request;
|
|
4631
4640
|
const _queryParams = {
|
|
4632
4641
|
userId,
|
|
4633
4642
|
orgId,
|
|
4634
4643
|
clientId,
|
|
4644
|
+
scope,
|
|
4635
4645
|
folderId,
|
|
4636
4646
|
startFrom,
|
|
4637
4647
|
limit
|
|
@@ -4689,9 +4699,10 @@ var DocumentsClient = class {
|
|
|
4689
4699
|
return HttpResponsePromise.fromPromise(this.__ingestDocument(request, requestOptions));
|
|
4690
4700
|
}
|
|
4691
4701
|
async __ingestDocument(request, requestOptions) {
|
|
4692
|
-
const { upsert, body: _body } = request;
|
|
4702
|
+
const { upsert, allowClear, body: _body } = request;
|
|
4693
4703
|
const _queryParams = {
|
|
4694
|
-
upsert
|
|
4704
|
+
upsert,
|
|
4705
|
+
allowClear
|
|
4695
4706
|
};
|
|
4696
4707
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
4697
4708
|
const _headers = mergeHeaders(
|
|
@@ -4815,7 +4826,10 @@ var DocumentsClient = class {
|
|
|
4815
4826
|
return HttpResponsePromise.fromPromise(this.__updateDocument(request, requestOptions));
|
|
4816
4827
|
}
|
|
4817
4828
|
async __updateDocument(request, requestOptions) {
|
|
4818
|
-
const { id, body: _body } = request;
|
|
4829
|
+
const { id, allowClear, body: _body } = request;
|
|
4830
|
+
const _queryParams = {
|
|
4831
|
+
allowClear
|
|
4832
|
+
};
|
|
4819
4833
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
4820
4834
|
const _headers = mergeHeaders(
|
|
4821
4835
|
_authRequest.headers,
|
|
@@ -4830,7 +4844,7 @@ var DocumentsClient = class {
|
|
|
4830
4844
|
method: "PUT",
|
|
4831
4845
|
headers: _headers,
|
|
4832
4846
|
contentType: "application/json",
|
|
4833
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
4847
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
4834
4848
|
requestType: "json",
|
|
4835
4849
|
body: _body,
|
|
4836
4850
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -7566,7 +7580,7 @@ var RecordsClient = class {
|
|
|
7566
7580
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/batch");
|
|
7567
7581
|
}
|
|
7568
7582
|
/**
|
|
7569
|
-
* Returns a paginated list of records in your account as a `{data, nextCursor}` page. Supply exactly one of `type`, `folderId`, or `recent=true` to choose the mode: `type` lists all records of a single type; `folderId` lists all records in a folder (any type); and `recent=true` returns the account-wide recently-updated feed across all types, newest first. You may combine `type` with `folderId` to list a single type within a folder. The owner filters (`userId`, `orgId`, `clientId`) further narrow the type and folder modes; the `recent` feed is standalone and ignores all filters. Each token only sees the record types it is scoped to read. Requires the `records:r` scope. By default the response returns the indexed projection of each record; set `includePayload=true` to include full payloads.
|
|
7583
|
+
* Returns a paginated list of records in your account as a `{data, nextCursor}` page. Supply exactly one of `type`, `folderId`, or `recent=true` to choose the mode: `type` lists all records of a single type; `folderId` lists all records in a folder (any type); and `recent=true` returns the account-wide recently-updated feed across all types, newest first. You may combine `type` with `folderId` to list a single type within a folder. The owner filters (`userId`, `orgId`, `clientId`, `scope`) further narrow the type and folder modes; the `recent` feed is standalone and ignores all filters. Each token only sees the record types it is scoped to read. Requires the `records:r` scope. By default the response returns the indexed projection of each record; set `includePayload=true` to include full payloads.
|
|
7570
7584
|
*
|
|
7571
7585
|
* @param {Vectros.ListRecordsRequest} request
|
|
7572
7586
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -7578,6 +7592,7 @@ var RecordsClient = class {
|
|
|
7578
7592
|
* userId: "550e8400-e29b-41d4-a716-446655440000",
|
|
7579
7593
|
* orgId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
7580
7594
|
* clientId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
7595
|
+
* scope: "group:eng-team",
|
|
7581
7596
|
* startFrom: "550e8400-e29b-41d4-a716-446655440000"
|
|
7582
7597
|
* })
|
|
7583
7598
|
*/
|
|
@@ -7585,13 +7600,25 @@ var RecordsClient = class {
|
|
|
7585
7600
|
return HttpResponsePromise.fromPromise(this.__listRecords(request, requestOptions));
|
|
7586
7601
|
}
|
|
7587
7602
|
async __listRecords(request = {}, requestOptions) {
|
|
7588
|
-
const {
|
|
7603
|
+
const {
|
|
7604
|
+
type: type_,
|
|
7605
|
+
folderId,
|
|
7606
|
+
userId,
|
|
7607
|
+
orgId,
|
|
7608
|
+
clientId,
|
|
7609
|
+
scope,
|
|
7610
|
+
startFrom,
|
|
7611
|
+
limit,
|
|
7612
|
+
includePayload,
|
|
7613
|
+
recent
|
|
7614
|
+
} = request;
|
|
7589
7615
|
const _queryParams = {
|
|
7590
7616
|
type: type_,
|
|
7591
7617
|
folderId,
|
|
7592
7618
|
userId,
|
|
7593
7619
|
orgId,
|
|
7594
7620
|
clientId,
|
|
7621
|
+
scope,
|
|
7595
7622
|
startFrom,
|
|
7596
7623
|
limit,
|
|
7597
7624
|
includePayload,
|
|
@@ -7656,9 +7683,10 @@ var RecordsClient = class {
|
|
|
7656
7683
|
return HttpResponsePromise.fromPromise(this.__createRecord(request, requestOptions));
|
|
7657
7684
|
}
|
|
7658
7685
|
async __createRecord(request, requestOptions) {
|
|
7659
|
-
const { upsert, body: _body } = request;
|
|
7686
|
+
const { upsert, allowClear, body: _body } = request;
|
|
7660
7687
|
const _queryParams = {
|
|
7661
|
-
upsert
|
|
7688
|
+
upsert,
|
|
7689
|
+
allowClear
|
|
7662
7690
|
};
|
|
7663
7691
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7664
7692
|
const _headers = mergeHeaders(
|
|
@@ -7780,7 +7808,10 @@ var RecordsClient = class {
|
|
|
7780
7808
|
return HttpResponsePromise.fromPromise(this.__updateRecord(request, requestOptions));
|
|
7781
7809
|
}
|
|
7782
7810
|
async __updateRecord(request, requestOptions) {
|
|
7783
|
-
const { id, body: _body } = request;
|
|
7811
|
+
const { id, allowClear, body: _body } = request;
|
|
7812
|
+
const _queryParams = {
|
|
7813
|
+
allowClear
|
|
7814
|
+
};
|
|
7784
7815
|
const _authRequest = await this._options.authProvider.getAuthRequest();
|
|
7785
7816
|
const _headers = mergeHeaders(
|
|
7786
7817
|
_authRequest.headers,
|
|
@@ -7795,7 +7826,7 @@ var RecordsClient = class {
|
|
|
7795
7826
|
method: "PUT",
|
|
7796
7827
|
headers: _headers,
|
|
7797
7828
|
contentType: "application/json",
|
|
7798
|
-
queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
|
|
7829
|
+
queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
|
|
7799
7830
|
requestType: "json",
|
|
7800
7831
|
body: _body,
|
|
7801
7832
|
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
|
|
@@ -8880,7 +8911,14 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
|
8880
8911
|
// null sentinel (+ ScopeClause). e.g. `{ orgId: ["org_x", null] }` =
|
|
8881
8912
|
// "org_x's records AND tenant-shared records". Omitting null restricts to
|
|
8882
8913
|
// the listed owners ONLY (the key will NOT see tenant-level/seed records).
|
|
8883
|
-
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional()
|
|
8914
|
+
dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
|
|
8915
|
+
// Optional identity overrides for the service principal's profile — scope
|
|
8916
|
+
// values its key STAMPS onto everything it writes (a credential can only
|
|
8917
|
+
// stamp ownership its identity carries). Keys: `orgId`/`clientId` (or the
|
|
8918
|
+
// namespaced `scope:<ns>` form); values may be `${{ identities.* }}` tokens
|
|
8919
|
+
// (substituted at apply time). e.g. `{ orgId: '${{ identities.team }}' }`
|
|
8920
|
+
// makes every record the service key writes org/team-owned.
|
|
8921
|
+
identityOverrides: import_zod.z.record(import_zod.z.string().min(1)).optional()
|
|
8884
8922
|
}).strict();
|
|
8885
8923
|
var BlueprintRoleClauseSchema = import_zod.z.object({
|
|
8886
8924
|
allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
|
|
@@ -8906,7 +8944,14 @@ var SeedCommonShape = {
|
|
|
8906
8944
|
* seeds resolve a `reference` against (across surfaces: a record seed may
|
|
8907
8945
|
* reference a document seed by its externalId, and vice versa).
|
|
8908
8946
|
*/
|
|
8909
|
-
externalId: import_zod.z.string().min(1)
|
|
8947
|
+
externalId: import_zod.z.string().min(1),
|
|
8948
|
+
/**
|
|
8949
|
+
* Optional scope ownership for the seeded item, as `namespace:value` entries
|
|
8950
|
+
* (≤2, e.g. `['org:${{ identities.team }}']`; tokens substitute at apply
|
|
8951
|
+
* time). `[]` = a private, user-owned item. Omitted = the seeding
|
|
8952
|
+
* credential's full identity (the default).
|
|
8953
|
+
*/
|
|
8954
|
+
scopes: import_zod.z.array(import_zod.z.string()).optional()
|
|
8910
8955
|
};
|
|
8911
8956
|
var RecordSeedSchema = import_zod.z.object({
|
|
8912
8957
|
surface: import_zod.z.literal("record"),
|
|
@@ -8930,25 +8975,25 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
|
|
|
8930
8975
|
]);
|
|
8931
8976
|
var SELF_TOKEN_RE = /\$\{\{\s*self\.[A-Za-z_]\w*\s*\}\}/;
|
|
8932
8977
|
function lintSelfTokenPlacement(value, ctx) {
|
|
8933
|
-
const walk = (node,
|
|
8978
|
+
const walk = (node, path5, inRoleDataScope) => {
|
|
8934
8979
|
if (typeof node === "string") {
|
|
8935
8980
|
if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
|
|
8936
8981
|
ctx.addIssue({
|
|
8937
8982
|
code: import_zod.z.ZodIssueCode.custom,
|
|
8938
|
-
path:
|
|
8983
|
+
path: path5,
|
|
8939
8984
|
message: "'${{ self.* }}' is a runtime per-principal placeholder \u2014 it is only valid inside a roles[].dataScope value"
|
|
8940
8985
|
});
|
|
8941
8986
|
}
|
|
8942
8987
|
return;
|
|
8943
8988
|
}
|
|
8944
8989
|
if (Array.isArray(node)) {
|
|
8945
|
-
node.forEach((v, i) => walk(v, [...
|
|
8990
|
+
node.forEach((v, i) => walk(v, [...path5, i], inRoleDataScope));
|
|
8946
8991
|
return;
|
|
8947
8992
|
}
|
|
8948
8993
|
if (node && typeof node === "object") {
|
|
8949
8994
|
for (const [k, v] of Object.entries(node)) {
|
|
8950
|
-
const entering = inRoleDataScope ||
|
|
8951
|
-
walk(v, [...
|
|
8995
|
+
const entering = inRoleDataScope || path5.length === 3 && path5[0] === "roles" && k === "dataScope";
|
|
8996
|
+
walk(v, [...path5, k], entering);
|
|
8952
8997
|
}
|
|
8953
8998
|
}
|
|
8954
8999
|
};
|
|
@@ -8959,13 +9004,13 @@ function lintIdentityRefsDeclared(value, ctx) {
|
|
|
8959
9004
|
const declared = new Set(
|
|
8960
9005
|
value && typeof value === "object" && "identities" in value && value.identities ? Object.keys(value.identities) : []
|
|
8961
9006
|
);
|
|
8962
|
-
const walk = (node,
|
|
9007
|
+
const walk = (node, path5) => {
|
|
8963
9008
|
if (typeof node === "string") {
|
|
8964
9009
|
for (const m of node.matchAll(IDENTITY_REF_RE)) {
|
|
8965
9010
|
if (!declared.has(m[1])) {
|
|
8966
9011
|
ctx.addIssue({
|
|
8967
9012
|
code: import_zod.z.ZodIssueCode.custom,
|
|
8968
|
-
path:
|
|
9013
|
+
path: path5,
|
|
8969
9014
|
message: `'\${{ identities.${m[1]} }}' references an undeclared identity \u2014 add '${m[1]}' to the top-level 'identities' block`
|
|
8970
9015
|
});
|
|
8971
9016
|
}
|
|
@@ -8973,11 +9018,11 @@ function lintIdentityRefsDeclared(value, ctx) {
|
|
|
8973
9018
|
return;
|
|
8974
9019
|
}
|
|
8975
9020
|
if (Array.isArray(node)) {
|
|
8976
|
-
node.forEach((v, i) => walk(v, [...
|
|
9021
|
+
node.forEach((v, i) => walk(v, [...path5, i]));
|
|
8977
9022
|
} else if (node && typeof node === "object") {
|
|
8978
9023
|
for (const [k, v] of Object.entries(node)) {
|
|
8979
|
-
if (
|
|
8980
|
-
walk(v, [...
|
|
9024
|
+
if (path5.length === 0 && k === "identities") continue;
|
|
9025
|
+
walk(v, [...path5, k]);
|
|
8981
9026
|
}
|
|
8982
9027
|
}
|
|
8983
9028
|
};
|
|
@@ -9024,9 +9069,9 @@ var BlueprintSchema = import_zod.z.object({
|
|
|
9024
9069
|
lintIdentityRefsDeclared(bp, ctx);
|
|
9025
9070
|
lintSeedSurfaces(bp, ctx);
|
|
9026
9071
|
});
|
|
9027
|
-
function formatIssuePath(
|
|
9072
|
+
function formatIssuePath(path5) {
|
|
9028
9073
|
let out = "";
|
|
9029
|
-
for (const seg of
|
|
9074
|
+
for (const seg of path5) {
|
|
9030
9075
|
if (typeof seg === "number") out += `[${seg}]`;
|
|
9031
9076
|
else out += out.length ? `.${seg}` : seg;
|
|
9032
9077
|
}
|
|
@@ -9064,6 +9109,14 @@ function contextNameOf(blueprint) {
|
|
|
9064
9109
|
return blueprint.contextName ?? `MCP \u2014 ${blueprint.name}`;
|
|
9065
9110
|
}
|
|
9066
9111
|
var IDENTITY_TOKEN_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
|
|
9112
|
+
var BlueprintIdentityError = class extends Error {
|
|
9113
|
+
issues;
|
|
9114
|
+
constructor(message, issues = []) {
|
|
9115
|
+
super(message);
|
|
9116
|
+
this.name = "BlueprintIdentityError";
|
|
9117
|
+
this.issues = issues;
|
|
9118
|
+
}
|
|
9119
|
+
};
|
|
9067
9120
|
function collectIdentityReferences(value) {
|
|
9068
9121
|
const found = /* @__PURE__ */ new Set();
|
|
9069
9122
|
const walk = (node) => {
|
|
@@ -9078,6 +9131,61 @@ function collectIdentityReferences(value) {
|
|
|
9078
9131
|
walk(value);
|
|
9079
9132
|
return [...found];
|
|
9080
9133
|
}
|
|
9134
|
+
function substitute(node, idMap) {
|
|
9135
|
+
if (typeof node === "string") {
|
|
9136
|
+
return node.replace(IDENTITY_TOKEN_RE, (_full, name) => idMap[name]);
|
|
9137
|
+
}
|
|
9138
|
+
if (Array.isArray(node)) return node.map((v) => substitute(v, idMap));
|
|
9139
|
+
if (node && typeof node === "object") {
|
|
9140
|
+
const out = {};
|
|
9141
|
+
for (const [k, v] of Object.entries(node)) out[k] = substitute(v, idMap);
|
|
9142
|
+
return out;
|
|
9143
|
+
}
|
|
9144
|
+
return node;
|
|
9145
|
+
}
|
|
9146
|
+
async function resolveBlueprintIdentities(raw, resolve) {
|
|
9147
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
9148
|
+
const { identities: rawIdentities, ...body } = raw;
|
|
9149
|
+
const refs = collectIdentityReferences(body);
|
|
9150
|
+
if (rawIdentities === void 0 && refs.length === 0) return raw;
|
|
9151
|
+
const parsed = IdentitiesDeclSchema.safeParse(rawIdentities ?? {});
|
|
9152
|
+
if (!parsed.success) {
|
|
9153
|
+
throw new BlueprintIdentityError(
|
|
9154
|
+
"Blueprint identities block is invalid",
|
|
9155
|
+
parsed.error.issues.map((i) => ({
|
|
9156
|
+
path: i.path.length ? `identities.${i.path.join(".")}` : "identities",
|
|
9157
|
+
message: i.message
|
|
9158
|
+
}))
|
|
9159
|
+
);
|
|
9160
|
+
}
|
|
9161
|
+
const declared = parsed.data;
|
|
9162
|
+
const undeclared = refs.filter((name) => !(name in declared));
|
|
9163
|
+
if (undeclared.length) {
|
|
9164
|
+
throw new BlueprintIdentityError(
|
|
9165
|
+
`Blueprint references undeclared identities: ${undeclared.join(", ")}`,
|
|
9166
|
+
undeclared.map((name) => ({
|
|
9167
|
+
path: `identities.${name}`,
|
|
9168
|
+
message: `'\${{ identities.${name} }}' is referenced but not declared in the 'identities' block`
|
|
9169
|
+
}))
|
|
9170
|
+
);
|
|
9171
|
+
}
|
|
9172
|
+
const idMap = {};
|
|
9173
|
+
for (const [name, decl] of Object.entries(declared)) {
|
|
9174
|
+
try {
|
|
9175
|
+
const id = await resolve(name, decl);
|
|
9176
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
9177
|
+
throw new Error(`resolver returned a non-string id (${JSON.stringify(id)})`);
|
|
9178
|
+
}
|
|
9179
|
+
idMap[name] = id;
|
|
9180
|
+
} catch (err) {
|
|
9181
|
+
throw new BlueprintIdentityError(
|
|
9182
|
+
`Failed to resolve identity '${name}' (${decl.kind} externalId=${decl.externalId}): ${err instanceof Error ? err.message : String(err)}`,
|
|
9183
|
+
[{ path: `identities.${name}`, message: err instanceof Error ? err.message : String(err) }]
|
|
9184
|
+
);
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
9187
|
+
return substitute(body, idMap);
|
|
9188
|
+
}
|
|
9081
9189
|
var INPUT_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9082
9190
|
var InputDeclSchema = import_zod2.z.object({
|
|
9083
9191
|
type: import_zod2.z.enum(["string", "number", "boolean"]),
|
|
@@ -9115,18 +9223,18 @@ var WHOLE_TOKEN_RE = /^\$\{\{\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*\}\}$/;
|
|
|
9115
9223
|
var TOKEN_SCAN_RE = /\$\$\{\{|\$\{\{([\s\S]*?)\}\}|\$\{\{/g;
|
|
9116
9224
|
var INNER_REF_RE = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/;
|
|
9117
9225
|
var DEFERRED_NAMESPACES = /* @__PURE__ */ new Set(["self", "identities"]);
|
|
9118
|
-
function resolveRef(ns, name, ctx,
|
|
9226
|
+
function resolveRef(ns, name, ctx, path5) {
|
|
9119
9227
|
if (ns === "inputs") {
|
|
9120
9228
|
if (!(name in ctx.declared)) {
|
|
9121
9229
|
ctx.issues.push({
|
|
9122
|
-
path:
|
|
9230
|
+
path: path5,
|
|
9123
9231
|
message: `unknown input 'inputs.${name}' \u2014 declare it in the top-level 'inputs:' block`
|
|
9124
9232
|
});
|
|
9125
9233
|
return void 0;
|
|
9126
9234
|
}
|
|
9127
9235
|
if (!(name in ctx.values)) {
|
|
9128
9236
|
ctx.issues.push({
|
|
9129
|
-
path:
|
|
9237
|
+
path: path5,
|
|
9130
9238
|
message: `input 'inputs.${name}' has no value \u2014 supply it (--set ${name}=\u2026 or --values) or give it a default`
|
|
9131
9239
|
});
|
|
9132
9240
|
return void 0;
|
|
@@ -9136,14 +9244,14 @@ function resolveRef(ns, name, ctx, path4) {
|
|
|
9136
9244
|
if (ns === "vectros") {
|
|
9137
9245
|
if (name !== "context" && name !== "suffix") {
|
|
9138
9246
|
ctx.issues.push({
|
|
9139
|
-
path:
|
|
9247
|
+
path: path5,
|
|
9140
9248
|
message: `unknown built-in 'vectros.${name}' (available: vectros.context, vectros.suffix)`
|
|
9141
9249
|
});
|
|
9142
9250
|
return void 0;
|
|
9143
9251
|
}
|
|
9144
9252
|
if (!ctx.builtins) {
|
|
9145
9253
|
ctx.issues.push({
|
|
9146
|
-
path:
|
|
9254
|
+
path: path5,
|
|
9147
9255
|
message: `'vectros.${name}' is unavailable: contextId must be a literal string to derive built-ins`
|
|
9148
9256
|
});
|
|
9149
9257
|
return void 0;
|
|
@@ -9151,12 +9259,12 @@ function resolveRef(ns, name, ctx, path4) {
|
|
|
9151
9259
|
return ctx.builtins[name];
|
|
9152
9260
|
}
|
|
9153
9261
|
ctx.issues.push({
|
|
9154
|
-
path:
|
|
9262
|
+
path: path5,
|
|
9155
9263
|
message: `unknown namespace '${ns}' in '\${{ ${ns}.${name} }}' (available: inputs, vectros)`
|
|
9156
9264
|
});
|
|
9157
9265
|
return void 0;
|
|
9158
9266
|
}
|
|
9159
|
-
function interpolate(str, ctx,
|
|
9267
|
+
function interpolate(str, ctx, path5) {
|
|
9160
9268
|
let out = "";
|
|
9161
9269
|
let last = 0;
|
|
9162
9270
|
TOKEN_SCAN_RE.lastIndex = 0;
|
|
@@ -9167,7 +9275,7 @@ function interpolate(str, ctx, path4) {
|
|
|
9167
9275
|
out += "${{";
|
|
9168
9276
|
} else if (m[0] === "${{" && m[1] === void 0) {
|
|
9169
9277
|
ctx.issues.push({
|
|
9170
|
-
path:
|
|
9278
|
+
path: path5,
|
|
9171
9279
|
message: `unterminated token '\${{' \u2014 expected a closing '}}' (escape a literal with '$\${{')`
|
|
9172
9280
|
});
|
|
9173
9281
|
} else {
|
|
@@ -9175,13 +9283,13 @@ function interpolate(str, ctx, path4) {
|
|
|
9175
9283
|
const ref = inner.match(INNER_REF_RE);
|
|
9176
9284
|
if (!ref) {
|
|
9177
9285
|
ctx.issues.push({
|
|
9178
|
-
path:
|
|
9286
|
+
path: path5,
|
|
9179
9287
|
message: `malformed reference '\${{ ${inner} }}' \u2014 expected '\${{ namespace.name }}' (escape a literal with '$\${{')`
|
|
9180
9288
|
});
|
|
9181
9289
|
} else if (DEFERRED_NAMESPACES.has(ref[1])) {
|
|
9182
9290
|
out += m[0];
|
|
9183
9291
|
} else {
|
|
9184
|
-
const v = resolveRef(ref[1], ref[2], ctx,
|
|
9292
|
+
const v = resolveRef(ref[1], ref[2], ctx, path5);
|
|
9185
9293
|
if (v !== void 0) out += String(v);
|
|
9186
9294
|
}
|
|
9187
9295
|
}
|
|
@@ -9190,34 +9298,34 @@ function interpolate(str, ctx, path4) {
|
|
|
9190
9298
|
out += str.slice(last);
|
|
9191
9299
|
return out;
|
|
9192
9300
|
}
|
|
9193
|
-
function substituteValue(val, ctx,
|
|
9301
|
+
function substituteValue(val, ctx, path5) {
|
|
9194
9302
|
if (typeof val === "string") {
|
|
9195
9303
|
const whole = val.match(WHOLE_TOKEN_RE);
|
|
9196
9304
|
if (whole) {
|
|
9197
9305
|
if (DEFERRED_NAMESPACES.has(whole[1])) return val;
|
|
9198
|
-
const v = resolveRef(whole[1], whole[2], ctx,
|
|
9306
|
+
const v = resolveRef(whole[1], whole[2], ctx, path5);
|
|
9199
9307
|
return v === void 0 ? val : v;
|
|
9200
9308
|
}
|
|
9201
|
-
return interpolate(val, ctx,
|
|
9309
|
+
return interpolate(val, ctx, path5);
|
|
9202
9310
|
}
|
|
9203
9311
|
if (Array.isArray(val)) {
|
|
9204
|
-
return val.map((v, i) => substituteValue(v, ctx, `${
|
|
9312
|
+
return val.map((v, i) => substituteValue(v, ctx, `${path5}[${i}]`));
|
|
9205
9313
|
}
|
|
9206
9314
|
if (val !== null && typeof val === "object") {
|
|
9207
9315
|
const out = {};
|
|
9208
9316
|
for (const [k, v] of Object.entries(val)) {
|
|
9209
|
-
out[k] = substituteValue(v, ctx,
|
|
9317
|
+
out[k] = substituteValue(v, ctx, path5 ? `${path5}.${k}` : k);
|
|
9210
9318
|
}
|
|
9211
9319
|
return out;
|
|
9212
9320
|
}
|
|
9213
9321
|
return val;
|
|
9214
9322
|
}
|
|
9215
9323
|
function coerce(raw, type, name, issues) {
|
|
9216
|
-
const
|
|
9324
|
+
const path5 = `inputs.${name}`;
|
|
9217
9325
|
if (type === "string") {
|
|
9218
9326
|
if (typeof raw === "string") return raw;
|
|
9219
9327
|
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
9220
|
-
issues.push({ path:
|
|
9328
|
+
issues.push({ path: path5, message: `expected a string value` });
|
|
9221
9329
|
return void 0;
|
|
9222
9330
|
}
|
|
9223
9331
|
if (type === "number") {
|
|
@@ -9225,12 +9333,12 @@ function coerce(raw, type, name, issues) {
|
|
|
9225
9333
|
if (typeof raw === "string") {
|
|
9226
9334
|
const n = Number(raw.trim());
|
|
9227
9335
|
if (raw.trim() === "" || Number.isNaN(n)) {
|
|
9228
|
-
issues.push({ path:
|
|
9336
|
+
issues.push({ path: path5, message: `expected a number, got '${raw}'` });
|
|
9229
9337
|
return void 0;
|
|
9230
9338
|
}
|
|
9231
9339
|
return n;
|
|
9232
9340
|
}
|
|
9233
|
-
issues.push({ path:
|
|
9341
|
+
issues.push({ path: path5, message: `expected a number` });
|
|
9234
9342
|
return void 0;
|
|
9235
9343
|
}
|
|
9236
9344
|
if (typeof raw === "boolean") return raw;
|
|
@@ -9238,10 +9346,10 @@ function coerce(raw, type, name, issues) {
|
|
|
9238
9346
|
const t = raw.trim().toLowerCase();
|
|
9239
9347
|
if (t === "true") return true;
|
|
9240
9348
|
if (t === "false") return false;
|
|
9241
|
-
issues.push({ path:
|
|
9349
|
+
issues.push({ path: path5, message: `expected a boolean ('true'|'false'), got '${raw}'` });
|
|
9242
9350
|
return void 0;
|
|
9243
9351
|
}
|
|
9244
|
-
issues.push({ path:
|
|
9352
|
+
issues.push({ path: path5, message: `expected a boolean` });
|
|
9245
9353
|
return void 0;
|
|
9246
9354
|
}
|
|
9247
9355
|
function resolveValues(declared, supplied, issues) {
|
|
@@ -9693,9 +9801,20 @@ var DATA_PLANE_ACTIONS = [
|
|
|
9693
9801
|
"folders:c"
|
|
9694
9802
|
];
|
|
9695
9803
|
var EDITOR_ACTIONS = [...DATA_PLANE_ACTIONS, "records:d", "documents:d", "folders:d"];
|
|
9804
|
+
var MEMBER_SHARED_READ_ACTIONS = [
|
|
9805
|
+
"records:r:control",
|
|
9806
|
+
"records:r:convention",
|
|
9807
|
+
"records:r:gotcha",
|
|
9808
|
+
"records:r:term",
|
|
9809
|
+
"documents:r",
|
|
9810
|
+
"search:r",
|
|
9811
|
+
"inference:r",
|
|
9812
|
+
"schemas:r",
|
|
9813
|
+
"folders:r"
|
|
9814
|
+
];
|
|
9696
9815
|
var agenticSdlc = {
|
|
9697
9816
|
name: "agentic-sdlc",
|
|
9698
|
-
version: "1.
|
|
9817
|
+
version: "1.6.0",
|
|
9699
9818
|
description: "A whole-SDLC system of record for an AI development team \u2014 decisions, designs, references, runbooks, post-mortems (as documents) plus controls, conventions, gotchas, and a glossary (as records), cross-linked and recalled by meaning.",
|
|
9700
9819
|
contextId: "agentic-sdlc",
|
|
9701
9820
|
contextName: "Agentic SDLC Knowledge Base",
|
|
@@ -10394,6 +10513,173 @@ var agenticSdlc = {
|
|
|
10394
10513
|
{ fieldName: "updatedOn", rangeEnabled: true },
|
|
10395
10514
|
"sourceRef"
|
|
10396
10515
|
]
|
|
10516
|
+
},
|
|
10517
|
+
{
|
|
10518
|
+
// ISOLATED AGENT MEMORY (the "other half" of the KB). Distinct from the
|
|
10519
|
+
// shared records above — which are the team's GLOBAL, curated knowledge —
|
|
10520
|
+
// `memory` is per-principal: a fact an agent (or the human it works for)
|
|
10521
|
+
// records for ITSELF, isolated by ownership via a `${{ self.userId }}`
|
|
10522
|
+
// role dataScope (the private tier; a team tier keyed on a shared scope is
|
|
10523
|
+
// a deliberate future addition, not baked in this version). Deliberately a
|
|
10524
|
+
// SINGLE flexible type so the recall hook queries ONE typeName; the flavor
|
|
10525
|
+
// is the `kind` field, mirroring the file-memory taxonomy it replaces, so
|
|
10526
|
+
// migration is 1:1. Isolation is enforced by the ROLE (type-scoped
|
|
10527
|
+
// `records:*:memory` actions + owner dataScope), NOT the schema — the
|
|
10528
|
+
// schema is tier-agnostic.
|
|
10529
|
+
typeName: "memory",
|
|
10530
|
+
displayName: "Memory",
|
|
10531
|
+
indexMode: "HYBRID",
|
|
10532
|
+
// keyword on the title + semantic on the body (the recalled fact)
|
|
10533
|
+
fields: [
|
|
10534
|
+
{
|
|
10535
|
+
fieldId: "title",
|
|
10536
|
+
fieldType: "string",
|
|
10537
|
+
required: true,
|
|
10538
|
+
searchable: true,
|
|
10539
|
+
validation: { minLength: 1, maxLength: 200 },
|
|
10540
|
+
description: 'A short handle for the memory (the file-memory "name" slug analog).',
|
|
10541
|
+
renderHints: { label: "Title", widget: "text", order: 1, displayField: true }
|
|
10542
|
+
},
|
|
10543
|
+
{
|
|
10544
|
+
fieldId: "body",
|
|
10545
|
+
fieldType: "string",
|
|
10546
|
+
searchable: true,
|
|
10547
|
+
description: "The memory itself \u2014 the durable fact, retrieved by meaning. The RAG-able body.",
|
|
10548
|
+
renderHints: { label: "Body", widget: "textarea", order: 2 }
|
|
10549
|
+
},
|
|
10550
|
+
{
|
|
10551
|
+
// The flavor of memory — mirrors the file-memory frontmatter `type:` so
|
|
10552
|
+
// a migration maps 1:1. `observation` is the episodic/scratch flavor
|
|
10553
|
+
// (TTL-able when the thread tier lands); the rest are durable.
|
|
10554
|
+
fieldId: "kind",
|
|
10555
|
+
fieldType: "enum",
|
|
10556
|
+
filterable: true,
|
|
10557
|
+
enumValues: ["user", "feedback", "project", "reference", "observation"],
|
|
10558
|
+
renderHints: { label: "Kind", widget: "select", order: 3 }
|
|
10559
|
+
},
|
|
10560
|
+
{
|
|
10561
|
+
// Structure axis 2 of 4: WHAT the memory is about — the same
|
|
10562
|
+
// subsystem/topic label every curated KB type carries, so one `area`
|
|
10563
|
+
// filter narrows recall across curated + memory content uniformly.
|
|
10564
|
+
fieldId: "area",
|
|
10565
|
+
fieldType: "string",
|
|
10566
|
+
filterable: true,
|
|
10567
|
+
description: `Subsystem/topic the memory applies to (e.g. "auth", "search") \u2014 matches the shared types' area vocabulary.`,
|
|
10568
|
+
renderHints: { label: "Area", widget: "text", order: 4 }
|
|
10569
|
+
},
|
|
10570
|
+
{
|
|
10571
|
+
// Structure axis 3: WHICH FUNCTION recorded it. A small stable ROLE
|
|
10572
|
+
// vocabulary (e.g. "pm", "builder", "reviewer") — not an instance id:
|
|
10573
|
+
// instances are already distinguished by the owning principal (who)
|
|
10574
|
+
// + threadId (which conversation).
|
|
10575
|
+
fieldId: "agent",
|
|
10576
|
+
fieldType: "string",
|
|
10577
|
+
filterable: true,
|
|
10578
|
+
description: 'The agent role that recorded it (e.g. "pm", "builder") \u2014 the function, not the instance.',
|
|
10579
|
+
renderHints: { label: "Agent", widget: "text", order: 5 }
|
|
10580
|
+
},
|
|
10581
|
+
{
|
|
10582
|
+
fieldId: "tags",
|
|
10583
|
+
fieldType: "array",
|
|
10584
|
+
filterable: true,
|
|
10585
|
+
description: 'Freeform labels for filtering recall (e.g. "auth", "testing").',
|
|
10586
|
+
renderHints: { label: "Tags", order: 6 }
|
|
10587
|
+
},
|
|
10588
|
+
{
|
|
10589
|
+
// The reversible supersede pattern shared with the records above — a
|
|
10590
|
+
// memory is retired by a status flip, never hard-deleted, so how the
|
|
10591
|
+
// agent's understanding evolved stays inspectable. Promotion to a
|
|
10592
|
+
// higher tier = supersede here + re-create at the new scope.
|
|
10593
|
+
fieldId: "status",
|
|
10594
|
+
fieldType: "enum",
|
|
10595
|
+
filterable: true,
|
|
10596
|
+
enumValues: ["active", "superseded"],
|
|
10597
|
+
renderHints: { label: "Status", widget: "select", order: 7 }
|
|
10598
|
+
},
|
|
10599
|
+
{
|
|
10600
|
+
// Structure axis 4: WHICH CONVERSATION (episodic slice). The caller's
|
|
10601
|
+
// session/thread identifier — whatever the agent runtime calls it. A
|
|
10602
|
+
// lookup filter WITHIN what the principal owns, NOT an isolation
|
|
10603
|
+
// boundary; sessions of the same principal share trust.
|
|
10604
|
+
fieldId: "threadId",
|
|
10605
|
+
fieldType: "string",
|
|
10606
|
+
filterable: true,
|
|
10607
|
+
description: "Optional conversation/thread id (your runtime's session identifier) \u2014 slices episodic memory within your own store.",
|
|
10608
|
+
renderHints: { label: "Thread", widget: "text", order: 8 }
|
|
10609
|
+
},
|
|
10610
|
+
{
|
|
10611
|
+
fieldId: "updatedOn",
|
|
10612
|
+
fieldType: "date",
|
|
10613
|
+
description: 'ISO-8601 \u2014 when last revised. Range-queryable \u2014 "what did I learn this week".',
|
|
10614
|
+
renderHints: { label: "Updated on", widget: "date", order: 9 }
|
|
10615
|
+
},
|
|
10616
|
+
{
|
|
10617
|
+
// Importance BAND for the always-load pinned set + recall ranking. Coarse bands
|
|
10618
|
+
// (0 normal / 10 pinned / 20 high / 30 critical) — pick a band, not a fiddly
|
|
10619
|
+
// integer, to avoid priority inflation. NULLABLE and set only on the elevated
|
|
10620
|
+
// subset, so the range-index cost lands only on that low-cardinality set (normal
|
|
10621
|
+
// memories write no range row). Orientation enumerates `priority order=desc
|
|
10622
|
+
// limit=X` — the read query IS the cap (overflow falls below the cutoff, never
|
|
10623
|
+
// evicted). MEMORY-ONLY: a pin is a memory that REFERENCES a curated item, it
|
|
10624
|
+
// never pins the curated item in place (pins = always-load; curated KB = recalled
|
|
10625
|
+
// when relevant).
|
|
10626
|
+
fieldId: "priority",
|
|
10627
|
+
fieldType: "number",
|
|
10628
|
+
filterable: true,
|
|
10629
|
+
description: "Importance band (0/10/20/30) for the always-load pinned set + recall ranking. Optional \u2014 set only on elevated memories.",
|
|
10630
|
+
renderHints: { label: "Priority", order: 10 }
|
|
10631
|
+
},
|
|
10632
|
+
{
|
|
10633
|
+
// Opaque provenance — the file / issue / URL this memory is ABOUT (records parity
|
|
10634
|
+
// with the curated types' sourceRef). Not a typed graph edge.
|
|
10635
|
+
fieldId: "sourceRef",
|
|
10636
|
+
fieldType: "string",
|
|
10637
|
+
description: "Provenance: the file path / issue / URL this memory is about.",
|
|
10638
|
+
renderHints: { label: "Source ref", widget: "text", order: 11 }
|
|
10639
|
+
},
|
|
10640
|
+
{
|
|
10641
|
+
// The evolution/promotion trail WITHIN memory (self-ref, SAME owner): the memory
|
|
10642
|
+
// that replaced this one. Pairs with ARCHIVE-retire. Promotion to the curated tier
|
|
10643
|
+
// is copy-up + archive-down with NO shared->private back-reference, so a curated
|
|
10644
|
+
// (shared) artifact never embeds a private memory's id/slug.
|
|
10645
|
+
fieldId: "supersededBy",
|
|
10646
|
+
fieldType: "reference",
|
|
10647
|
+
targetTypeName: "memory",
|
|
10648
|
+
targetSurface: "record",
|
|
10649
|
+
targetField: "externalId",
|
|
10650
|
+
cardinality: "one",
|
|
10651
|
+
description: "The memory that superseded this one (by externalId) \u2014 the evolution trail.",
|
|
10652
|
+
renderHints: { label: "Superseded by", order: 12 }
|
|
10653
|
+
},
|
|
10654
|
+
{
|
|
10655
|
+
// The `[[see-also]]` link between a principal's own memories (self-ref, SAME
|
|
10656
|
+
// owner). One-cardinality in this version (the proven reference shape); a
|
|
10657
|
+
// many-cardinality see-also graph is a later extension.
|
|
10658
|
+
fieldId: "relatedTo",
|
|
10659
|
+
fieldType: "reference",
|
|
10660
|
+
targetTypeName: "memory",
|
|
10661
|
+
targetSurface: "record",
|
|
10662
|
+
targetField: "externalId",
|
|
10663
|
+
cardinality: "one",
|
|
10664
|
+
description: "A related memory (by externalId) \u2014 the see-also link within your own store.",
|
|
10665
|
+
renderHints: { label: "Related to", order: 13 }
|
|
10666
|
+
}
|
|
10667
|
+
],
|
|
10668
|
+
// externalId is the memory's stable slug (the file-memory `name:`) — the idempotent
|
|
10669
|
+
// upsert key — sent top-level, not declared as a field.
|
|
10670
|
+
// ⚖ LEAN by design: `memory` is the HIGHEST-VOLUME, churniest record here, so it only
|
|
10671
|
+
// carries lookups for fields we ENUMERATE DETERMINISTICLY. `area`/`agent`/`status` are
|
|
10672
|
+
// `filterable` (typed SEARCH metadata — narrow recall without a per-write lookup row),
|
|
10673
|
+
// NOT lookups; `sourceRef` is a plain provenance field. A typical write pays just
|
|
10674
|
+
// `kind` (eq) + `updatedOn` (range); `threadId`/`priority` cost only when set (both
|
|
10675
|
+
// nullable). Contrast the curated types, which carry more lookups because they are
|
|
10676
|
+
// low-volume and repo-synced (`sourceRef` enumeration).
|
|
10677
|
+
lookupFields: [
|
|
10678
|
+
"kind",
|
|
10679
|
+
"threadId",
|
|
10680
|
+
{ fieldName: "updatedOn", rangeEnabled: true },
|
|
10681
|
+
{ fieldName: "priority", rangeEnabled: true }
|
|
10682
|
+
]
|
|
10397
10683
|
}
|
|
10398
10684
|
],
|
|
10399
10685
|
// Least-privilege, data-plane only. The scope of the `ssk_*` key the bootstrap
|
|
@@ -10416,7 +10702,41 @@ var agenticSdlc = {
|
|
|
10416
10702
|
// owner may permanently remove genuine strays. Still no control-plane action, so
|
|
10417
10703
|
// the scope gate accepts it as a data-plane-only role.
|
|
10418
10704
|
roles: {
|
|
10419
|
-
|
|
10705
|
+
// The human curator. NOTE: this role is deliberately UNSCOPED (whole-context),
|
|
10706
|
+
// which includes members' private memory — the trusted-administrator tradeoff,
|
|
10707
|
+
// called out in the bundled guide. Scope it down if your deployment wants
|
|
10708
|
+
// curator access without private-memory visibility.
|
|
10709
|
+
editor: [{ allowedActions: EDITOR_ACTIONS }],
|
|
10710
|
+
// A team member (or their agent) — TWO memory tiers as two UNIONed clauses
|
|
10711
|
+
// (a record is accessible when ANY clause grants it; within one clause every
|
|
10712
|
+
// dataScope dimension must match):
|
|
10713
|
+
// 1) the CURATED shared KB (+ semantic recall). UNSCOPED, but safe: search
|
|
10714
|
+
// and grounded answers are enforced per row against the per-type action
|
|
10715
|
+
// qualifier, so recall over this clause admits only the content it grants
|
|
10716
|
+
// a TYPED read for (the curated record types + documents) — never the
|
|
10717
|
+
// `memory` type, so a member's recall never returns another principal's
|
|
10718
|
+
// private memory.
|
|
10719
|
+
// 2) PRIVATE memory: the member's OWN `memory` records, isolated per-request
|
|
10720
|
+
// by `${{ self.userId }}` (the principal dimension — the stable ownership
|
|
10721
|
+
// axis, unaffected by the evolution of shared/team scopes).
|
|
10722
|
+
// Team-shared *working* memory (a group-owned tier every member reads and
|
|
10723
|
+
// writes) is a planned addition, intentionally NOT in this version: it rides
|
|
10724
|
+
// the ownership axis for shared scopes, which is being finalized, and this
|
|
10725
|
+
// blueprint avoids baking a shape that is about to change. Bind with
|
|
10726
|
+
// `vectros join agentic-sdlc --role member`; verify a binding with
|
|
10727
|
+
// `vectros access explain --principal me --context agentic-sdlc`.
|
|
10728
|
+
member: [
|
|
10729
|
+
{ allowedActions: MEMBER_SHARED_READ_ACTIONS },
|
|
10730
|
+
{
|
|
10731
|
+
// `inference:r` is IN this self-scoped clause (not only clause 1) so a
|
|
10732
|
+
// member's `rag_ask` can ground on its OWN private memory — the read
|
|
10733
|
+
// grant (`records:r:memory`) and the inference capability travel in the
|
|
10734
|
+
// SAME clause, so grounding works regardless of cross-clause admission
|
|
10735
|
+
// semantics. Still self-fenced by dataScope: never another principal's.
|
|
10736
|
+
allowedActions: ["records:cru:memory", "search:r", "inference:r"],
|
|
10737
|
+
dataScope: { userId: ["${{ self.userId }}"] }
|
|
10738
|
+
}
|
|
10739
|
+
]
|
|
10420
10740
|
},
|
|
10421
10741
|
servicePrincipal: {
|
|
10422
10742
|
externalId: "agentic-sdlc",
|
|
@@ -10844,13 +11164,13 @@ async function mintBridgeToken(opts) {
|
|
|
10844
11164
|
fetchImpl = fetch
|
|
10845
11165
|
} = opts;
|
|
10846
11166
|
const base = apiBaseUrl.replace(/\/$/, "");
|
|
10847
|
-
const
|
|
10848
|
-
const url = `${base}${
|
|
11167
|
+
const path5 = bridgeTokenPath(useScopedToken);
|
|
11168
|
+
const url = `${base}${path5}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
|
|
10849
11169
|
return fetchStToken(
|
|
10850
11170
|
url,
|
|
10851
11171
|
cognitoToken,
|
|
10852
11172
|
fetchImpl,
|
|
10853
|
-
|
|
11173
|
+
path5,
|
|
10854
11174
|
(status) => status === 404 && !useScopedToken ? ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)` : ""
|
|
10855
11175
|
);
|
|
10856
11176
|
}
|
|
@@ -11356,6 +11676,54 @@ async function readPersistedKey(pack, dir = vectrosHomeDir()) {
|
|
|
11356
11676
|
}
|
|
11357
11677
|
}
|
|
11358
11678
|
|
|
11679
|
+
// src/identity-resolver.ts
|
|
11680
|
+
function userPayload(decl) {
|
|
11681
|
+
const meta = decl.metadata;
|
|
11682
|
+
if (decl.displayName === void 0) return meta;
|
|
11683
|
+
return { displayName: decl.displayName, ...meta ?? {} };
|
|
11684
|
+
}
|
|
11685
|
+
function requireId(id, kind, externalId) {
|
|
11686
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
11687
|
+
throw new Error(`identity ${kind} '${externalId}' create returned no id \u2014 cannot substitute its token.`);
|
|
11688
|
+
}
|
|
11689
|
+
return id;
|
|
11690
|
+
}
|
|
11691
|
+
function makeIdentityResolver(client, log) {
|
|
11692
|
+
return async (name, decl) => {
|
|
11693
|
+
switch (decl.kind) {
|
|
11694
|
+
case "user": {
|
|
11695
|
+
const u = await client.identity.createUser({
|
|
11696
|
+
body: { externalId: decl.externalId, type: "HUMAN", payload: userPayload(decl) }
|
|
11697
|
+
});
|
|
11698
|
+
const id = requireId(u.id, "user", decl.externalId);
|
|
11699
|
+
log.debug({ component: "identity-resolver", name, kind: "user", externalId: decl.externalId, id }, "identity ensured");
|
|
11700
|
+
return id;
|
|
11701
|
+
}
|
|
11702
|
+
case "org": {
|
|
11703
|
+
const o = await client.identity.createOrg({
|
|
11704
|
+
body: { externalId: decl.externalId, name: decl.displayName, payload: decl.metadata }
|
|
11705
|
+
});
|
|
11706
|
+
const id = requireId(o.id, "org", decl.externalId);
|
|
11707
|
+
log.debug({ component: "identity-resolver", name, kind: "org", externalId: decl.externalId, id }, "identity ensured");
|
|
11708
|
+
return id;
|
|
11709
|
+
}
|
|
11710
|
+
case "client": {
|
|
11711
|
+
const c = await client.identity.createClient({
|
|
11712
|
+
body: { externalId: decl.externalId, name: decl.displayName, payload: decl.metadata }
|
|
11713
|
+
});
|
|
11714
|
+
const id = requireId(c.id, "client", decl.externalId);
|
|
11715
|
+
log.debug({ component: "identity-resolver", name, kind: "client", externalId: decl.externalId, id }, "identity ensured");
|
|
11716
|
+
return id;
|
|
11717
|
+
}
|
|
11718
|
+
}
|
|
11719
|
+
};
|
|
11720
|
+
}
|
|
11721
|
+
async function resolveApplyTimeIdentities(blueprint, client, log) {
|
|
11722
|
+
const resolver = makeIdentityResolver(client, log);
|
|
11723
|
+
const resolved = await resolveBlueprintIdentities(blueprint, resolver);
|
|
11724
|
+
return parseBlueprint(resolved);
|
|
11725
|
+
}
|
|
11726
|
+
|
|
11359
11727
|
// src/loader.ts
|
|
11360
11728
|
var LostKeyError = class extends Error {
|
|
11361
11729
|
constructor(keyName) {
|
|
@@ -11447,7 +11815,7 @@ function assertNoUnresolvedIdentities(blueprint) {
|
|
|
11447
11815
|
if (hasBlock || refs.length > 0) {
|
|
11448
11816
|
const detail = hasBlock ? `declares an 'identities' block` : `references ${refs.map((r) => `\${{ identities.${r} }}`).join(", ")}`;
|
|
11449
11817
|
throw new UnresolvedIdentitiesError(
|
|
11450
|
-
`Blueprint ${detail}
|
|
11818
|
+
`Blueprint ${detail} that reached the loader UNRESOLVED \u2014 apply-time identity resolution did not run before provisioning (an internal wiring error). Applying it would write literal '\${{ identities.* }}' tokens into ownership/dataScope fields. This is a defense-in-depth backstop; resolution happens in the CLI apply orchestrator (resolveApplyTimeIdentities) before the loader.`
|
|
11451
11819
|
);
|
|
11452
11820
|
}
|
|
11453
11821
|
}
|
|
@@ -11519,7 +11887,12 @@ async function runLoader(client, input) {
|
|
|
11519
11887
|
// to the SDK's loosely-typed Record<string, Record<string,unknown>>.
|
|
11520
11888
|
data_scope: blueprint.accessProfile.dataScope
|
|
11521
11889
|
}
|
|
11522
|
-
]
|
|
11890
|
+
],
|
|
11891
|
+
// Optional identity overrides — scope values the service key STAMPS onto
|
|
11892
|
+
// everything it writes (e.g. the blueprint's team org, so the curated
|
|
11893
|
+
// knowledge it seeds/writes is org-owned). `${{ identities.* }}` tokens
|
|
11894
|
+
// were substituted before the loader ran.
|
|
11895
|
+
identityOverrides: blueprint.accessProfile.identityOverrides
|
|
11523
11896
|
// NB: `status` is intentionally omitted — the SDK's body type does not model it
|
|
11524
11897
|
// and the platform defaults a new profile to 'active' (the only status the loader
|
|
11525
11898
|
// ever wants). Sending it was redundant.
|
|
@@ -11595,7 +11968,9 @@ async function runLoader(client, input) {
|
|
|
11595
11968
|
schemaId,
|
|
11596
11969
|
externalId: rec.externalId,
|
|
11597
11970
|
// Optional structured payload (the document's `fields`); omitted when absent.
|
|
11598
|
-
payload: rec.fields
|
|
11971
|
+
payload: rec.fields,
|
|
11972
|
+
// Optional scope ownership (`namespace:value` entries; `[]` = private).
|
|
11973
|
+
scopes: rec.scopes
|
|
11599
11974
|
}
|
|
11600
11975
|
});
|
|
11601
11976
|
if (created.id) seededDocumentIds.push(created.id);
|
|
@@ -11605,7 +11980,8 @@ async function runLoader(client, input) {
|
|
|
11605
11980
|
typeName: rec.typeName,
|
|
11606
11981
|
schemaId,
|
|
11607
11982
|
externalId: rec.externalId,
|
|
11608
|
-
payload: rec.fields
|
|
11983
|
+
payload: rec.fields,
|
|
11984
|
+
scopes: rec.scopes
|
|
11609
11985
|
}
|
|
11610
11986
|
});
|
|
11611
11987
|
if (created.id) seededRecordIds.push(created.id);
|
|
@@ -11766,7 +12142,6 @@ function validateBlueprint(input) {
|
|
|
11766
12142
|
for (const clauses of Object.values(blueprint.roles ?? {})) {
|
|
11767
12143
|
for (const clause of clauses) assertScopeGate(clause.allowedActions);
|
|
11768
12144
|
}
|
|
11769
|
-
assertNoUnresolvedIdentities(blueprint);
|
|
11770
12145
|
return blueprint;
|
|
11771
12146
|
}
|
|
11772
12147
|
var BLANK_SLATE = {
|
|
@@ -12022,13 +12397,6 @@ ${BOOTSTRAP_HELP}
|
|
|
12022
12397
|
process.stderr.write(" Minting nothing.\n");
|
|
12023
12398
|
return 2;
|
|
12024
12399
|
}
|
|
12025
|
-
if (err instanceof UnresolvedIdentitiesError) {
|
|
12026
|
-
process.stderr.write(`
|
|
12027
|
-
\u2716 ${err.message}
|
|
12028
|
-
Minting nothing.
|
|
12029
|
-
`);
|
|
12030
|
-
return 2;
|
|
12031
|
-
}
|
|
12032
12400
|
throw err;
|
|
12033
12401
|
}
|
|
12034
12402
|
const gate = evaluateScopeGate(blueprint.accessProfile.allowedActions);
|
|
@@ -12053,6 +12421,14 @@ ${BOOTSTRAP_HELP}
|
|
|
12053
12421
|
process.stdout.write(`
|
|
12054
12422
|
${renderPlan(blueprint, planSteps)}
|
|
12055
12423
|
`);
|
|
12424
|
+
const declaredIdentities = Object.entries(blueprint.identities ?? {});
|
|
12425
|
+
if (declaredIdentities.length > 0) {
|
|
12426
|
+
process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
|
|
12427
|
+
for (const [name, decl] of declaredIdentities) {
|
|
12428
|
+
process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
|
|
12429
|
+
`);
|
|
12430
|
+
}
|
|
12431
|
+
}
|
|
12056
12432
|
if (!args.yes && !args.print && isTty) {
|
|
12057
12433
|
const ans = (await promptCtx.prompt("\n Apply this plan? [y/N]: ")).trim().toLowerCase();
|
|
12058
12434
|
if (ans !== "y" && ans !== "yes") {
|
|
@@ -12077,17 +12453,25 @@ ${renderPlan(blueprint, planSteps)}
|
|
|
12077
12453
|
});
|
|
12078
12454
|
process.stdout.write(` \u2714 Bootstrap token minted (5-min TTL, ${args.tenant} tenant)
|
|
12079
12455
|
`);
|
|
12456
|
+
const pingIdentity = deps.resolveIdentity ?? resolveIdentity;
|
|
12457
|
+
const resolveIdentities = deps.resolveApplyTimeIdentities ?? resolveApplyTimeIdentities;
|
|
12458
|
+
const loader = deps.runLoader ?? runLoader;
|
|
12080
12459
|
const client = new VectrosClient({ token: bridge.token, environment: baseUrl });
|
|
12081
|
-
const identity = await
|
|
12460
|
+
const identity = await pingIdentity({ log, apiKey: bridge.token, environment: baseUrl });
|
|
12082
12461
|
const tenantId = identity.tenantId;
|
|
12083
12462
|
if (!tenantId || typeof tenantId !== "string") {
|
|
12084
12463
|
throw new Error(
|
|
12085
12464
|
"Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed for bootstrap."
|
|
12086
12465
|
);
|
|
12087
12466
|
}
|
|
12088
|
-
const
|
|
12089
|
-
const result = await runLoader(client, {
|
|
12467
|
+
const resolvedBlueprint = await resolveIdentities(
|
|
12090
12468
|
blueprint,
|
|
12469
|
+
client,
|
|
12470
|
+
log
|
|
12471
|
+
);
|
|
12472
|
+
const machineId = await resolveMachineId();
|
|
12473
|
+
const result = await loader(client, {
|
|
12474
|
+
blueprint: resolvedBlueprint,
|
|
12091
12475
|
tenantId,
|
|
12092
12476
|
machineId,
|
|
12093
12477
|
rotate: args.rotate,
|
|
@@ -12165,6 +12549,13 @@ ${codeAddHint}`);
|
|
|
12165
12549
|
`);
|
|
12166
12550
|
return 3;
|
|
12167
12551
|
}
|
|
12552
|
+
if (err instanceof BlueprintIdentityError || err instanceof UnresolvedIdentitiesError) {
|
|
12553
|
+
process.stderr.write(`
|
|
12554
|
+
\u2716 ${err.message}
|
|
12555
|
+
No context or key was provisioned.
|
|
12556
|
+
`);
|
|
12557
|
+
return 2;
|
|
12558
|
+
}
|
|
12168
12559
|
process.stderr.write(`
|
|
12169
12560
|
\u2716 Bootstrap failed: ${err instanceof Error ? err.message : String(err)}
|
|
12170
12561
|
`);
|
|
@@ -12444,13 +12835,6 @@ async function resolveAndValidate(arg, verb, supplied) {
|
|
|
12444
12835
|
}
|
|
12445
12836
|
throw err;
|
|
12446
12837
|
}
|
|
12447
|
-
try {
|
|
12448
|
-
assertNoUnresolvedIdentities(blueprint);
|
|
12449
|
-
} catch (err) {
|
|
12450
|
-
process.stderr.write(` \u2716 ${err instanceof Error ? err.message : String(err)}
|
|
12451
|
-
`);
|
|
12452
|
-
return null;
|
|
12453
|
-
}
|
|
12454
12838
|
const findings = lintBlueprint(blueprint);
|
|
12455
12839
|
for (const f of findings) {
|
|
12456
12840
|
const mark = f.severity === "error" ? "\u2716" : "\u26A0";
|
|
@@ -12498,6 +12882,14 @@ async function runPlan(args) {
|
|
|
12498
12882
|
process.stdout.write(`
|
|
12499
12883
|
${renderPlan(blueprint, steps)}
|
|
12500
12884
|
`);
|
|
12885
|
+
const declaredIdentities = Object.entries(blueprint.identities ?? {});
|
|
12886
|
+
if (declaredIdentities.length > 0) {
|
|
12887
|
+
process.stdout.write("\n Identities to ensure (tenant-wide, idempotent by external id):\n");
|
|
12888
|
+
for (const [name, decl] of declaredIdentities) {
|
|
12889
|
+
process.stdout.write(` ~ ${name} (${decl.kind}) \u2014 external id '${decl.externalId}'
|
|
12890
|
+
`);
|
|
12891
|
+
}
|
|
12892
|
+
}
|
|
12501
12893
|
const setEcho = [...args.set].join(" --set ");
|
|
12502
12894
|
process.stdout.write(
|
|
12503
12895
|
`
|
|
@@ -12696,6 +13088,18 @@ function normalizePrincipalUserId(principal) {
|
|
|
12696
13088
|
function splitCsv(csv) {
|
|
12697
13089
|
return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12698
13090
|
}
|
|
13091
|
+
function parseJsonObjectFlag(flag, raw) {
|
|
13092
|
+
let parsed;
|
|
13093
|
+
try {
|
|
13094
|
+
parsed = JSON.parse(raw);
|
|
13095
|
+
} catch {
|
|
13096
|
+
throw new Error(`${flag} is not valid JSON: ${raw}`);
|
|
13097
|
+
}
|
|
13098
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
13099
|
+
throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
|
|
13100
|
+
}
|
|
13101
|
+
return parsed;
|
|
13102
|
+
}
|
|
12699
13103
|
|
|
12700
13104
|
// src/paginate.ts
|
|
12701
13105
|
var PAGE_LIMIT = 100;
|
|
@@ -13191,6 +13595,23 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
13191
13595
|
process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
|
|
13192
13596
|
return 2;
|
|
13193
13597
|
}
|
|
13598
|
+
if (hasActions && splitCsv(opts.actions).length === 0) {
|
|
13599
|
+
process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
|
|
13600
|
+
return 2;
|
|
13601
|
+
}
|
|
13602
|
+
let identityOverrides;
|
|
13603
|
+
if (opts.org || opts.identityOverrides) {
|
|
13604
|
+
try {
|
|
13605
|
+
identityOverrides = {
|
|
13606
|
+
...opts.org ? { orgId: opts.org } : {},
|
|
13607
|
+
...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
|
|
13608
|
+
};
|
|
13609
|
+
} catch (err) {
|
|
13610
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
13611
|
+
`);
|
|
13612
|
+
return 2;
|
|
13613
|
+
}
|
|
13614
|
+
}
|
|
13194
13615
|
if (opts.principal !== SELF_PRINCIPAL_ALIAS) {
|
|
13195
13616
|
try {
|
|
13196
13617
|
assertPrincipalShape(opts.principal);
|
|
@@ -13207,10 +13628,12 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
13207
13628
|
const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
|
|
13208
13629
|
const profile = await client.auth.createAccessProfile({
|
|
13209
13630
|
contextId: opts.context,
|
|
13631
|
+
upsert: true,
|
|
13210
13632
|
body: {
|
|
13211
13633
|
principalId: principal,
|
|
13212
13634
|
roleId: opts.role,
|
|
13213
|
-
scopes
|
|
13635
|
+
scopes,
|
|
13636
|
+
identityOverrides
|
|
13214
13637
|
}
|
|
13215
13638
|
});
|
|
13216
13639
|
if (opts.json) {
|
|
@@ -13333,6 +13756,166 @@ ${renderProfile(p)}
|
|
|
13333
13756
|
});
|
|
13334
13757
|
}
|
|
13335
13758
|
|
|
13759
|
+
// src/commands/access-explain.ts
|
|
13760
|
+
var SELF = /* @__PURE__ */ Symbol("self");
|
|
13761
|
+
function canonicalDim(key) {
|
|
13762
|
+
if (key === "userId" || key === "partnerUserId") return "user";
|
|
13763
|
+
if (key === "orgId") return "scope:org";
|
|
13764
|
+
if (key === "clientId") return "scope:client";
|
|
13765
|
+
return key;
|
|
13766
|
+
}
|
|
13767
|
+
var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
|
|
13768
|
+
function normalizeClauses(raw) {
|
|
13769
|
+
const out = [];
|
|
13770
|
+
for (const c of raw) {
|
|
13771
|
+
if (!c || typeof c !== "object") continue;
|
|
13772
|
+
const obj = c;
|
|
13773
|
+
const actions = obj.allowed_actions ?? obj.allowedActions ?? [];
|
|
13774
|
+
const ds = obj.data_scope ?? obj.dataScope ?? {};
|
|
13775
|
+
const dataScope = {};
|
|
13776
|
+
for (const [k, v] of Object.entries(ds)) {
|
|
13777
|
+
const values = Array.isArray(v) ? v : [v];
|
|
13778
|
+
dataScope[canonicalDim(k)] = values.map((val) => {
|
|
13779
|
+
if (val === null) return null;
|
|
13780
|
+
const s = String(val);
|
|
13781
|
+
return SELF_TOKEN_RE2.test(s) ? SELF : s;
|
|
13782
|
+
});
|
|
13783
|
+
}
|
|
13784
|
+
out.push({ actions: Array.isArray(actions) ? actions.map(String) : [], dataScope });
|
|
13785
|
+
}
|
|
13786
|
+
return out;
|
|
13787
|
+
}
|
|
13788
|
+
function buildArchetypes(clauses) {
|
|
13789
|
+
const rows = [
|
|
13790
|
+
{ label: "a record you own alone (private)", owners: { user: SELF } },
|
|
13791
|
+
{ label: "another user's private record", owners: { user: "someone-else" } }
|
|
13792
|
+
];
|
|
13793
|
+
const scopeDims = /* @__PURE__ */ new Map();
|
|
13794
|
+
for (const c of clauses) {
|
|
13795
|
+
for (const [dim, values] of Object.entries(c.dataScope)) {
|
|
13796
|
+
if (dim === "user") continue;
|
|
13797
|
+
const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
|
|
13798
|
+
for (const v of values) if (v !== null) set.add(v);
|
|
13799
|
+
scopeDims.set(dim, set);
|
|
13800
|
+
}
|
|
13801
|
+
}
|
|
13802
|
+
for (const [dim, values] of scopeDims) {
|
|
13803
|
+
for (const v of values) {
|
|
13804
|
+
const shown = v === SELF ? `your ${dim.replace("scope:", "")}` : `${dim}=${String(v)}`;
|
|
13805
|
+
rows.push({ label: `yours, shared to ${shown}`, owners: { user: SELF, [dim]: v } });
|
|
13806
|
+
rows.push({ label: `another user's, shared to ${shown}`, owners: { user: "someone-else", [dim]: v } });
|
|
13807
|
+
rows.push({ label: `${shown} only (no user owner)`, owners: { [dim]: v } });
|
|
13808
|
+
}
|
|
13809
|
+
rows.push({
|
|
13810
|
+
label: `another user's, in a DIFFERENT ${dim.replace("scope:", "")}`,
|
|
13811
|
+
owners: { user: "someone-else", [dim]: `other-${dim.replace("scope:", "")}` }
|
|
13812
|
+
});
|
|
13813
|
+
}
|
|
13814
|
+
rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
|
|
13815
|
+
return rows;
|
|
13816
|
+
}
|
|
13817
|
+
function clauseMatches(clause, arch) {
|
|
13818
|
+
for (const [dim, allowed] of Object.entries(clause.dataScope)) {
|
|
13819
|
+
const recordValue = arch.owners[dim];
|
|
13820
|
+
const ok = allowed.some((a) => {
|
|
13821
|
+
if (a === null) return recordValue === void 0;
|
|
13822
|
+
if (a === SELF) return recordValue === SELF;
|
|
13823
|
+
return recordValue === a;
|
|
13824
|
+
});
|
|
13825
|
+
if (!ok) return false;
|
|
13826
|
+
}
|
|
13827
|
+
return true;
|
|
13828
|
+
}
|
|
13829
|
+
function explainClauses(clauses) {
|
|
13830
|
+
const archetypes = buildArchetypes(clauses);
|
|
13831
|
+
return archetypes.map((arch) => {
|
|
13832
|
+
const matched = [];
|
|
13833
|
+
const actions = /* @__PURE__ */ new Set();
|
|
13834
|
+
clauses.forEach((c, i) => {
|
|
13835
|
+
if (clauseMatches(c, arch)) {
|
|
13836
|
+
matched.push(i + 1);
|
|
13837
|
+
for (const a of c.actions) actions.add(a);
|
|
13838
|
+
}
|
|
13839
|
+
});
|
|
13840
|
+
return { archetype: arch.label, clauses: matched, actions: [...actions] };
|
|
13841
|
+
});
|
|
13842
|
+
}
|
|
13843
|
+
function renderDataScope(ds) {
|
|
13844
|
+
const entries = Object.entries(ds);
|
|
13845
|
+
if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
|
|
13846
|
+
return entries.map(
|
|
13847
|
+
([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
|
|
13848
|
+
).join(" AND ");
|
|
13849
|
+
}
|
|
13850
|
+
var SELF_PRINCIPAL_ALIAS2 = "me";
|
|
13851
|
+
async function runAccessExplain(opts, deps = {}) {
|
|
13852
|
+
return withAuth(opts, deps, async (auth) => {
|
|
13853
|
+
const client = auth.client;
|
|
13854
|
+
try {
|
|
13855
|
+
let principal = opts.principal;
|
|
13856
|
+
if (principal === SELF_PRINCIPAL_ALIAS2) {
|
|
13857
|
+
const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
|
|
13858
|
+
if (!self2) {
|
|
13859
|
+
process.stderr.write(
|
|
13860
|
+
" \u2716 Could not resolve your own user id from your login. Pass --principal usr_<id> explicitly.\n"
|
|
13861
|
+
);
|
|
13862
|
+
return 1;
|
|
13863
|
+
}
|
|
13864
|
+
principal = self2.principalId;
|
|
13865
|
+
}
|
|
13866
|
+
const profile = await client.auth.getAccessProfile({ contextId: opts.context, principalId: principal });
|
|
13867
|
+
let rawClauses = profile.scopes ?? [];
|
|
13868
|
+
let via = "inline scopes";
|
|
13869
|
+
if (profile.roleId) {
|
|
13870
|
+
const role = await client.auth.getRole({ contextId: opts.context, roleId: profile.roleId });
|
|
13871
|
+
rawClauses = role.scopes ?? [];
|
|
13872
|
+
via = `role '${profile.roleId}'`;
|
|
13873
|
+
}
|
|
13874
|
+
const clauses = normalizeClauses(rawClauses);
|
|
13875
|
+
const rows = explainClauses(clauses);
|
|
13876
|
+
if (opts.json) {
|
|
13877
|
+
process.stdout.write(
|
|
13878
|
+
`${formatJson({
|
|
13879
|
+
principal,
|
|
13880
|
+
context: opts.context,
|
|
13881
|
+
via,
|
|
13882
|
+
identityOverrides: profile.identityOverrides,
|
|
13883
|
+
clauses: clauses.map((c, i) => ({ n: i + 1, actions: c.actions, dataScope: renderDataScope(c.dataScope) })),
|
|
13884
|
+
matrix: rows,
|
|
13885
|
+
note: "Ownership dimensions only: per-type action qualifiers narrow further, and search/RAG visibility is bounded per row by the same clause's typed read grants. Client-side preview; the platform decides at request time."
|
|
13886
|
+
})}
|
|
13887
|
+
`
|
|
13888
|
+
);
|
|
13889
|
+
return 0;
|
|
13890
|
+
}
|
|
13891
|
+
process.stdout.write(`
|
|
13892
|
+
${principal} in '${opts.context}' \u2014 via ${via}
|
|
13893
|
+
`);
|
|
13894
|
+
if (profile.identityOverrides && Object.keys(profile.identityOverrides).length > 0) {
|
|
13895
|
+
process.stdout.write(` identity overrides: ${JSON.stringify(profile.identityOverrides)}
|
|
13896
|
+
`);
|
|
13897
|
+
}
|
|
13898
|
+
process.stdout.write("\n Clauses (a record is accessible when ANY clause grants it):\n");
|
|
13899
|
+
clauses.forEach((c, i) => {
|
|
13900
|
+
process.stdout.write(` ${i + 1}. [${c.actions.join(", ")}] where ${renderDataScope(c.dataScope)}
|
|
13901
|
+
`);
|
|
13902
|
+
});
|
|
13903
|
+
process.stdout.write(
|
|
13904
|
+
"\n" + formatTable(rows, [
|
|
13905
|
+
{ header: "record owned by\u2026", get: (r) => r.archetype },
|
|
13906
|
+
{ header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
|
|
13907
|
+
{ header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
|
|
13908
|
+
]) + "\n\n NOTE: this matrix models the OWNERSHIP dimensions only. Per-type qualifiers narrow further:\n a `records:<ops>:<type>` action reaches only that type, and search/RAG visibility is bounded\n per row by the SAME clause's typed read grants \u2014 a row shown with `search:r` is searchable\n only for the content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n"
|
|
13909
|
+
);
|
|
13910
|
+
return 0;
|
|
13911
|
+
} catch (err) {
|
|
13912
|
+
process.stderr.write(` \u2716 access explain failed: ${formatError(err)}
|
|
13913
|
+
`);
|
|
13914
|
+
return 1;
|
|
13915
|
+
}
|
|
13916
|
+
});
|
|
13917
|
+
}
|
|
13918
|
+
|
|
13336
13919
|
// src/commands/role.ts
|
|
13337
13920
|
function renderRole(r) {
|
|
13338
13921
|
return formatKeyValues([
|
|
@@ -13349,6 +13932,16 @@ async function runRoleCreate(opts, deps = {}) {
|
|
|
13349
13932
|
process.stderr.write(" \u2716 --actions must list at least one action (e.g. records:cru,search:r).\n");
|
|
13350
13933
|
return 2;
|
|
13351
13934
|
}
|
|
13935
|
+
let dataScope;
|
|
13936
|
+
if (opts.dataScope !== void 0) {
|
|
13937
|
+
try {
|
|
13938
|
+
dataScope = parseJsonObjectFlag("--data-scope", opts.dataScope);
|
|
13939
|
+
} catch (err) {
|
|
13940
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
13941
|
+
`);
|
|
13942
|
+
return 2;
|
|
13943
|
+
}
|
|
13944
|
+
}
|
|
13352
13945
|
return withAuth(opts, deps, async (auth) => {
|
|
13353
13946
|
const client = auth.client;
|
|
13354
13947
|
try {
|
|
@@ -13358,7 +13951,11 @@ async function runRoleCreate(opts, deps = {}) {
|
|
|
13358
13951
|
roleId: opts.roleId,
|
|
13359
13952
|
name: opts.name,
|
|
13360
13953
|
description: opts.description,
|
|
13361
|
-
|
|
13954
|
+
// data_scope is included only when supplied — the clause wire shape
|
|
13955
|
+
// stays byte-identical for unscoped roles.
|
|
13956
|
+
scopes: [
|
|
13957
|
+
dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
|
|
13958
|
+
]
|
|
13362
13959
|
}
|
|
13363
13960
|
});
|
|
13364
13961
|
if (opts.json) {
|
|
@@ -13446,6 +14043,7 @@ async function runRoleDelete(opts, deps = {}) {
|
|
|
13446
14043
|
}
|
|
13447
14044
|
|
|
13448
14045
|
// src/commands/context.ts
|
|
14046
|
+
var readline2 = __toESM(require("readline/promises"));
|
|
13449
14047
|
function renderContext(c) {
|
|
13450
14048
|
return formatKeyValues([
|
|
13451
14049
|
["context", c.contextId],
|
|
@@ -13507,6 +14105,80 @@ async function runContextList(opts, deps = {}) {
|
|
|
13507
14105
|
}
|
|
13508
14106
|
});
|
|
13509
14107
|
}
|
|
14108
|
+
async function runContextDestroy(opts, deps = {}) {
|
|
14109
|
+
const { contextId } = opts;
|
|
14110
|
+
if (!opts.force) {
|
|
14111
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
14112
|
+
if (!isTty) {
|
|
14113
|
+
process.stderr.write(
|
|
14114
|
+
` \u2716 context destroy is irreversible and needs an interactive confirmation.
|
|
14115
|
+
Re-run with --force to destroy '${contextId}' without a prompt.
|
|
14116
|
+
`
|
|
14117
|
+
);
|
|
14118
|
+
return 2;
|
|
14119
|
+
}
|
|
14120
|
+
const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
|
|
14121
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
14122
|
+
return { prompt: (q) => rl.question(q), close: () => rl.close() };
|
|
14123
|
+
})();
|
|
14124
|
+
try {
|
|
14125
|
+
process.stdout.write(
|
|
14126
|
+
`
|
|
14127
|
+
\u26A0 Destroying app context '${contextId}' permanently erases it AND all of its
|
|
14128
|
+
records, documents, folders, schemas, roles, and access profiles, and revokes
|
|
14129
|
+
its scoped keys. This cannot be undone.
|
|
14130
|
+
|
|
14131
|
+
`
|
|
14132
|
+
);
|
|
14133
|
+
const typed = await promptCtx.prompt(` Type the context id ('${contextId}') to confirm: `);
|
|
14134
|
+
if (typed.trim() !== contextId) {
|
|
14135
|
+
process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing destroyed.
|
|
14136
|
+
`);
|
|
14137
|
+
return 1;
|
|
14138
|
+
}
|
|
14139
|
+
} finally {
|
|
14140
|
+
promptCtx.close();
|
|
14141
|
+
}
|
|
14142
|
+
}
|
|
14143
|
+
return withAuth({ ...opts, provisioning: true }, deps, async (auth) => {
|
|
14144
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
14145
|
+
const url = `${auth.baseUrl.replace(/\/$/, "")}/developer/app-contexts/${encodeURIComponent(contextId)}?tenant=${encodeURIComponent(auth.tenant)}&confirm=${encodeURIComponent(contextId)}`;
|
|
14146
|
+
try {
|
|
14147
|
+
const res = await fetchImpl(url, {
|
|
14148
|
+
method: "DELETE",
|
|
14149
|
+
headers: { Authorization: `Bearer ${auth.cognitoToken}` }
|
|
14150
|
+
});
|
|
14151
|
+
if (!res.ok) {
|
|
14152
|
+
const body = await res.text().catch(() => "");
|
|
14153
|
+
let message = body;
|
|
14154
|
+
try {
|
|
14155
|
+
const parsed = JSON.parse(body);
|
|
14156
|
+
if (parsed.message) message = parsed.message;
|
|
14157
|
+
} catch {
|
|
14158
|
+
}
|
|
14159
|
+
process.stderr.write(` \u2716 context destroy failed: HTTP ${res.status}${message ? ` \u2014 ${message}` : ""}
|
|
14160
|
+
`);
|
|
14161
|
+
return 1;
|
|
14162
|
+
}
|
|
14163
|
+
if (opts.json) {
|
|
14164
|
+
process.stdout.write(`${formatJson({ contextId, status: "purging" })}
|
|
14165
|
+
`);
|
|
14166
|
+
return 0;
|
|
14167
|
+
}
|
|
14168
|
+
process.stdout.write(
|
|
14169
|
+
`
|
|
14170
|
+
\u2714 teardown of '${contextId}' started \u2014 the context and its contents are being
|
|
14171
|
+
deleted asynchronously (it reports status 'purging' until the drain completes)
|
|
14172
|
+
`
|
|
14173
|
+
);
|
|
14174
|
+
return 0;
|
|
14175
|
+
} catch (err) {
|
|
14176
|
+
process.stderr.write(` \u2716 context destroy failed: ${formatError(err)}
|
|
14177
|
+
`);
|
|
14178
|
+
return 1;
|
|
14179
|
+
}
|
|
14180
|
+
});
|
|
14181
|
+
}
|
|
13510
14182
|
async function runContextGet(opts, deps = {}) {
|
|
13511
14183
|
return withAuth({ ...opts, context: opts.contextId }, deps, async (auth) => {
|
|
13512
14184
|
const client = auth.client;
|
|
@@ -13915,7 +14587,12 @@ async function runTeardown(client, blueprint, machineId, snapshot, result) {
|
|
|
13915
14587
|
return steps;
|
|
13916
14588
|
}
|
|
13917
14589
|
async function runBlueprintHarness(client, input, deps) {
|
|
13918
|
-
const {
|
|
14590
|
+
const { tenantId, machineId, vectrosDir, keep = false, log, remintForContext } = input;
|
|
14591
|
+
const blueprint = await resolveApplyTimeIdentities(
|
|
14592
|
+
input.blueprint,
|
|
14593
|
+
client,
|
|
14594
|
+
log
|
|
14595
|
+
);
|
|
13919
14596
|
const remint = remintForContext ?? (async () => client);
|
|
13920
14597
|
let snapshot;
|
|
13921
14598
|
if (remintForContext) {
|
|
@@ -14089,11 +14766,461 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
|
|
|
14089
14766
|
});
|
|
14090
14767
|
}
|
|
14091
14768
|
|
|
14769
|
+
// src/keyring.ts
|
|
14770
|
+
var import_node_fs4 = require("fs");
|
|
14771
|
+
var import_node_path5 = __toESM(require("path"));
|
|
14772
|
+
var import_node_crypto4 = require("crypto");
|
|
14773
|
+
var KEYRING_FILE = "keyring.json";
|
|
14774
|
+
var SALT_FILE = "keyring.salt";
|
|
14775
|
+
var SECRETS_DIR = "secrets";
|
|
14776
|
+
var ALGO = "aes-256-gcm";
|
|
14777
|
+
var IV_LEN = 12;
|
|
14778
|
+
var TAG_LEN = 16;
|
|
14779
|
+
function keyringPath(dir = vectrosHomeDir()) {
|
|
14780
|
+
return import_node_path5.default.join(dir, KEYRING_FILE);
|
|
14781
|
+
}
|
|
14782
|
+
var CryptoFileSecretStore = class {
|
|
14783
|
+
dir;
|
|
14784
|
+
keyPromise;
|
|
14785
|
+
constructor(dir = vectrosHomeDir()) {
|
|
14786
|
+
this.dir = dir;
|
|
14787
|
+
}
|
|
14788
|
+
secretsDir() {
|
|
14789
|
+
return import_node_path5.default.join(this.dir, SECRETS_DIR);
|
|
14790
|
+
}
|
|
14791
|
+
secretPath(ref) {
|
|
14792
|
+
return import_node_path5.default.join(this.secretsDir(), `${encodeURIComponent(ref)}.enc`);
|
|
14793
|
+
}
|
|
14794
|
+
/** Resolve (and cache) the machine-derived AES key, creating the salt once. */
|
|
14795
|
+
async encryptionKey() {
|
|
14796
|
+
if (!this.keyPromise) {
|
|
14797
|
+
this.keyPromise = (async () => {
|
|
14798
|
+
const machineId = await resolveMachineId(this.dir);
|
|
14799
|
+
const salt = await this.readOrCreateSalt();
|
|
14800
|
+
return (0, import_node_crypto4.scryptSync)(machineId, salt, 32);
|
|
14801
|
+
})();
|
|
14802
|
+
}
|
|
14803
|
+
return this.keyPromise;
|
|
14804
|
+
}
|
|
14805
|
+
async readOrCreateSalt() {
|
|
14806
|
+
const saltPath = import_node_path5.default.join(this.dir, SALT_FILE);
|
|
14807
|
+
try {
|
|
14808
|
+
const existing = await import_node_fs4.promises.readFile(saltPath);
|
|
14809
|
+
if (existing.length >= 16) return existing;
|
|
14810
|
+
} catch {
|
|
14811
|
+
}
|
|
14812
|
+
const salt = (0, import_node_crypto4.randomBytes)(16);
|
|
14813
|
+
await import_node_fs4.promises.mkdir(this.dir, { recursive: true });
|
|
14814
|
+
await import_node_fs4.promises.writeFile(saltPath, salt, { mode: 384 });
|
|
14815
|
+
return salt;
|
|
14816
|
+
}
|
|
14817
|
+
async setSecret(ref, secret) {
|
|
14818
|
+
const key = await this.encryptionKey();
|
|
14819
|
+
const iv = (0, import_node_crypto4.randomBytes)(IV_LEN);
|
|
14820
|
+
const cipher = (0, import_node_crypto4.createCipheriv)(ALGO, key, iv);
|
|
14821
|
+
const enc = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
|
|
14822
|
+
const tag = cipher.getAuthTag();
|
|
14823
|
+
const blob = Buffer.concat([iv, tag, enc]);
|
|
14824
|
+
await import_node_fs4.promises.mkdir(this.secretsDir(), { recursive: true });
|
|
14825
|
+
await import_node_fs4.promises.writeFile(this.secretPath(ref), blob.toString("base64"), {
|
|
14826
|
+
encoding: "utf8",
|
|
14827
|
+
mode: 384
|
|
14828
|
+
});
|
|
14829
|
+
}
|
|
14830
|
+
async getSecret(ref) {
|
|
14831
|
+
let blob;
|
|
14832
|
+
try {
|
|
14833
|
+
blob = Buffer.from((await import_node_fs4.promises.readFile(this.secretPath(ref), "utf8")).trim(), "base64");
|
|
14834
|
+
} catch {
|
|
14835
|
+
return void 0;
|
|
14836
|
+
}
|
|
14837
|
+
if (blob.length <= IV_LEN + TAG_LEN) return void 0;
|
|
14838
|
+
try {
|
|
14839
|
+
const key = await this.encryptionKey();
|
|
14840
|
+
const iv = blob.subarray(0, IV_LEN);
|
|
14841
|
+
const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
14842
|
+
const enc = blob.subarray(IV_LEN + TAG_LEN);
|
|
14843
|
+
const decipher = (0, import_node_crypto4.createDecipheriv)(ALGO, key, iv);
|
|
14844
|
+
decipher.setAuthTag(tag);
|
|
14845
|
+
return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
|
|
14846
|
+
} catch {
|
|
14847
|
+
return void 0;
|
|
14848
|
+
}
|
|
14849
|
+
}
|
|
14850
|
+
async deleteSecret(ref) {
|
|
14851
|
+
try {
|
|
14852
|
+
await import_node_fs4.promises.rm(this.secretPath(ref));
|
|
14853
|
+
} catch {
|
|
14854
|
+
}
|
|
14855
|
+
}
|
|
14856
|
+
};
|
|
14857
|
+
async function readKeyring(dir = vectrosHomeDir()) {
|
|
14858
|
+
let raw;
|
|
14859
|
+
try {
|
|
14860
|
+
raw = await import_node_fs4.promises.readFile(keyringPath(dir), "utf8");
|
|
14861
|
+
} catch (err) {
|
|
14862
|
+
if (err.code === "ENOENT") return { version: 1, entries: [] };
|
|
14863
|
+
throw err;
|
|
14864
|
+
}
|
|
14865
|
+
let parsed;
|
|
14866
|
+
try {
|
|
14867
|
+
parsed = JSON.parse(raw);
|
|
14868
|
+
} catch {
|
|
14869
|
+
throw new Error(`Keyring index at ${keyringPath(dir)} is corrupt (invalid JSON). Fix or remove the file.`);
|
|
14870
|
+
}
|
|
14871
|
+
if (!Array.isArray(parsed.entries)) {
|
|
14872
|
+
throw new Error(`Keyring index at ${keyringPath(dir)} is malformed (no entries array). Fix or remove the file.`);
|
|
14873
|
+
}
|
|
14874
|
+
return { version: 1, active: parsed.active, entries: parsed.entries };
|
|
14875
|
+
}
|
|
14876
|
+
async function writeKeyring(kr, dir = vectrosHomeDir()) {
|
|
14877
|
+
await import_node_fs4.promises.mkdir(dir, { recursive: true });
|
|
14878
|
+
const finalPath = keyringPath(dir);
|
|
14879
|
+
const tmpPath = `${finalPath}.${process.pid}.${(0, import_node_crypto4.randomBytes)(4).toString("hex")}.tmp`;
|
|
14880
|
+
await import_node_fs4.promises.writeFile(tmpPath, `${JSON.stringify(kr, null, 2)}
|
|
14881
|
+
`, { encoding: "utf8", mode: 384 });
|
|
14882
|
+
await import_node_fs4.promises.rename(tmpPath, finalPath);
|
|
14883
|
+
}
|
|
14884
|
+
var Keyring = class {
|
|
14885
|
+
dir;
|
|
14886
|
+
store;
|
|
14887
|
+
constructor(deps = {}) {
|
|
14888
|
+
this.dir = deps.dir ?? vectrosHomeDir();
|
|
14889
|
+
this.store = deps.store ?? new CryptoFileSecretStore(this.dir);
|
|
14890
|
+
}
|
|
14891
|
+
/**
|
|
14892
|
+
* This machine's stable id — used to namespace a scoped-key NAME so each
|
|
14893
|
+
* machine holds its OWN independently-rotatable `ssk_*` under one shared
|
|
14894
|
+
* (idempotent) AccessProfile (see `machine-id.ts`). Resolved from the SAME
|
|
14895
|
+
* dir the secrets live in, so a test keyring rooted at a temp dir stays
|
|
14896
|
+
* self-consistent.
|
|
14897
|
+
*/
|
|
14898
|
+
async machineId() {
|
|
14899
|
+
return resolveMachineId(this.dir);
|
|
14900
|
+
}
|
|
14901
|
+
async list() {
|
|
14902
|
+
return (await readKeyring(this.dir)).entries;
|
|
14903
|
+
}
|
|
14904
|
+
async get(alias) {
|
|
14905
|
+
return (await this.list()).find((e) => e.alias === alias);
|
|
14906
|
+
}
|
|
14907
|
+
async activeAlias() {
|
|
14908
|
+
return (await readKeyring(this.dir)).active;
|
|
14909
|
+
}
|
|
14910
|
+
async getActive() {
|
|
14911
|
+
const kr = await readKeyring(this.dir);
|
|
14912
|
+
return kr.active ? kr.entries.find((e) => e.alias === kr.active) : void 0;
|
|
14913
|
+
}
|
|
14914
|
+
/**
|
|
14915
|
+
* Upsert an entry (by alias) and store its secret. Activates it by default —
|
|
14916
|
+
* `join` wants the freshly-created identity to become current.
|
|
14917
|
+
*/
|
|
14918
|
+
async add(entry, secret, opts = {}) {
|
|
14919
|
+
const kr = await readKeyring(this.dir);
|
|
14920
|
+
const prior = kr.entries.find((e) => e.alias === entry.alias);
|
|
14921
|
+
await this.store.setSecret(entry.secretRef, secret);
|
|
14922
|
+
if (prior && prior.secretRef !== entry.secretRef) {
|
|
14923
|
+
await this.store.deleteSecret(prior.secretRef);
|
|
14924
|
+
}
|
|
14925
|
+
const entries = kr.entries.filter((e) => e.alias !== entry.alias);
|
|
14926
|
+
entries.push(entry);
|
|
14927
|
+
const active = opts.activate === false ? kr.active : entry.alias;
|
|
14928
|
+
await writeKeyring({ version: 1, active, entries }, this.dir);
|
|
14929
|
+
}
|
|
14930
|
+
/** Point the active alias at an existing entry. Throws if the alias is unknown. */
|
|
14931
|
+
async setActive(alias) {
|
|
14932
|
+
const kr = await readKeyring(this.dir);
|
|
14933
|
+
if (!kr.entries.some((e) => e.alias === alias)) {
|
|
14934
|
+
throw new Error(`No keyring entry named '${alias}'. Run \`vectros keyring list\` to see entries.`);
|
|
14935
|
+
}
|
|
14936
|
+
await writeKeyring({ ...kr, version: 1, active: alias }, this.dir);
|
|
14937
|
+
}
|
|
14938
|
+
/** Remove an entry + its secret. Clears the active pointer if it named this alias. Returns true if removed. */
|
|
14939
|
+
async remove(alias) {
|
|
14940
|
+
const kr = await readKeyring(this.dir);
|
|
14941
|
+
const entry = kr.entries.find((e) => e.alias === alias);
|
|
14942
|
+
if (!entry) return false;
|
|
14943
|
+
await this.store.deleteSecret(entry.secretRef);
|
|
14944
|
+
const entries = kr.entries.filter((e) => e.alias !== alias);
|
|
14945
|
+
const active = kr.active === alias ? void 0 : kr.active;
|
|
14946
|
+
await writeKeyring({ version: 1, active, entries }, this.dir);
|
|
14947
|
+
return true;
|
|
14948
|
+
}
|
|
14949
|
+
/** The raw secret for a named entry (via the secret store), or undefined. */
|
|
14950
|
+
async resolveSecret(alias) {
|
|
14951
|
+
const entry = await this.get(alias);
|
|
14952
|
+
return entry ? this.store.getSecret(entry.secretRef) : void 0;
|
|
14953
|
+
}
|
|
14954
|
+
/** The raw secret for the active entry, or undefined if none is active. */
|
|
14955
|
+
async resolveActiveSecret() {
|
|
14956
|
+
const entry = await this.getActive();
|
|
14957
|
+
return entry ? this.store.getSecret(entry.secretRef) : void 0;
|
|
14958
|
+
}
|
|
14959
|
+
};
|
|
14960
|
+
|
|
14961
|
+
// src/commands/join.ts
|
|
14962
|
+
var SELF_ALIAS = "me";
|
|
14963
|
+
async function resolvePrincipal(as, auth) {
|
|
14964
|
+
const raw = as ?? SELF_ALIAS;
|
|
14965
|
+
if (raw === SELF_ALIAS) {
|
|
14966
|
+
const self2 = await resolveSelfPrincipal(auth.client, auth.cognitoToken);
|
|
14967
|
+
if (!self2) {
|
|
14968
|
+
throw new Error(
|
|
14969
|
+
"Could not resolve your own user id from your login. Pass --as usr_<id> explicitly (find it with `vectros identity list --type user`)."
|
|
14970
|
+
);
|
|
14971
|
+
}
|
|
14972
|
+
return self2.principalId;
|
|
14973
|
+
}
|
|
14974
|
+
if (!raw.startsWith("usr_")) {
|
|
14975
|
+
throw new Error(`--as must be 'me' or 'usr_<id>' (got '${raw}').`);
|
|
14976
|
+
}
|
|
14977
|
+
return raw;
|
|
14978
|
+
}
|
|
14979
|
+
async function issueOrRotateKey(client, tenantId, context, userId, keyName) {
|
|
14980
|
+
const first = await client.auth.createScopedKey({ keyName, tenantId, contextId: context, userId });
|
|
14981
|
+
if (first.rawKey) return first;
|
|
14982
|
+
const existing = (await drainPages(() => client.auth.listScopedKeys())).find(
|
|
14983
|
+
(k) => k.contextId === context && k.userId === userId && k.keyName === keyName && k.status === "active"
|
|
14984
|
+
);
|
|
14985
|
+
if (existing?.keyId) await client.auth.revokeScopedKey({ keyId: existing.keyId });
|
|
14986
|
+
const rotated = await client.auth.createScopedKey({ keyName, tenantId, contextId: context, userId });
|
|
14987
|
+
if (!rotated.rawKey) {
|
|
14988
|
+
throw new Error(
|
|
14989
|
+
"The key already exists and its secret could not be re-minted (the prior key may not have cleared yet). Retry shortly."
|
|
14990
|
+
);
|
|
14991
|
+
}
|
|
14992
|
+
return rotated;
|
|
14993
|
+
}
|
|
14994
|
+
async function runJoin(opts, deps = {}) {
|
|
14995
|
+
const hasRole = Boolean(opts.role);
|
|
14996
|
+
const hasActions = Boolean(opts.actions);
|
|
14997
|
+
if (hasRole === hasActions) {
|
|
14998
|
+
process.stderr.write(" \u2716 Provide exactly one of --role <roleId> or --actions <csv>.\n");
|
|
14999
|
+
return 2;
|
|
15000
|
+
}
|
|
15001
|
+
if (hasActions && splitCsv(opts.actions).length === 0) {
|
|
15002
|
+
process.stderr.write(" \u2716 --actions needs at least one action (an empty/blank list grants nothing).\n");
|
|
15003
|
+
return 2;
|
|
15004
|
+
}
|
|
15005
|
+
if (opts.as && opts.as !== SELF_ALIAS && !opts.as.startsWith("usr_")) {
|
|
15006
|
+
process.stderr.write(` \u2716 --as must be 'me' or 'usr_<id>' (got '${opts.as}').
|
|
15007
|
+
`);
|
|
15008
|
+
return 2;
|
|
15009
|
+
}
|
|
15010
|
+
const context = opts.contextArg;
|
|
15011
|
+
const alias = opts.alias ?? context;
|
|
15012
|
+
const keyring = deps.keyring ?? new Keyring();
|
|
15013
|
+
return withAuth({ ...opts, context }, deps, async (auth) => {
|
|
15014
|
+
const client = auth.client;
|
|
15015
|
+
try {
|
|
15016
|
+
const tenantId = requireTenantId(auth);
|
|
15017
|
+
const principalId = await resolvePrincipal(opts.as, auth);
|
|
15018
|
+
const userId = normalizePrincipalUserId(principalId);
|
|
15019
|
+
const scopes = hasActions ? [{ allowed_actions: splitCsv(opts.actions) }] : void 0;
|
|
15020
|
+
await client.auth.createAccessProfile({
|
|
15021
|
+
contextId: context,
|
|
15022
|
+
upsert: true,
|
|
15023
|
+
body: {
|
|
15024
|
+
principalId,
|
|
15025
|
+
roleId: opts.role,
|
|
15026
|
+
scopes,
|
|
15027
|
+
identityOverrides: opts.org ? { orgId: opts.org } : void 0
|
|
15028
|
+
}
|
|
15029
|
+
});
|
|
15030
|
+
const existingEntry = await keyring.get(alias);
|
|
15031
|
+
const heldSecret = existingEntry ? await keyring.resolveSecret(alias) : void 0;
|
|
15032
|
+
const canReuse = existingEntry && heldSecret && existingEntry.context === context && existingEntry.principalId === principalId && existingEntry.env === auth.env;
|
|
15033
|
+
if (canReuse && existingEntry) {
|
|
15034
|
+
await keyring.setActive(alias);
|
|
15035
|
+
emitJoined(alias, context, principalId, opts.role, existingEntry.keyId, true);
|
|
15036
|
+
return 0;
|
|
15037
|
+
}
|
|
15038
|
+
if (existingEntry) {
|
|
15039
|
+
process.stdout.write(
|
|
15040
|
+
`
|
|
15041
|
+
\u26A0 Replacing keyring entry '${alias}' (was ${existingEntry.principalId} in '${existingEntry.context}' @ ${existingEntry.env}). Pass --alias to keep both.
|
|
15042
|
+
`
|
|
15043
|
+
);
|
|
15044
|
+
}
|
|
15045
|
+
const keyName = keyNameFor(context, await keyring.machineId());
|
|
15046
|
+
const key = await issueOrRotateKey(client, tenantId, context, userId, keyName);
|
|
15047
|
+
if (!key.keyId) {
|
|
15048
|
+
throw new Error(
|
|
15049
|
+
"The minted key has no id \u2014 it could not be recorded or later rotated. Retry, or use `vectros key issue`."
|
|
15050
|
+
);
|
|
15051
|
+
}
|
|
15052
|
+
const entry = {
|
|
15053
|
+
alias,
|
|
15054
|
+
context,
|
|
15055
|
+
principalId,
|
|
15056
|
+
role: opts.role,
|
|
15057
|
+
keyId: key.keyId,
|
|
15058
|
+
env: auth.env,
|
|
15059
|
+
secretRef: key.keyId,
|
|
15060
|
+
createdAt: nowIso()
|
|
15061
|
+
};
|
|
15062
|
+
await keyring.add(entry, key.rawKey, { activate: true });
|
|
15063
|
+
emitJoined(alias, context, principalId, opts.role, entry.keyId, false);
|
|
15064
|
+
return 0;
|
|
15065
|
+
} catch (err) {
|
|
15066
|
+
process.stderr.write(` \u2716 join failed: ${formatError(err)}
|
|
15067
|
+
`);
|
|
15068
|
+
return 1;
|
|
15069
|
+
}
|
|
15070
|
+
});
|
|
15071
|
+
}
|
|
15072
|
+
function nowIso() {
|
|
15073
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
15074
|
+
}
|
|
15075
|
+
function emitJoined(alias, context, principalId, role, keyId, reused) {
|
|
15076
|
+
process.stdout.write(
|
|
15077
|
+
`
|
|
15078
|
+
\u2714 Joined '${context}' as ${principalId}` + (reused ? " (existing key reused)" : "") + "\n" + formatKeyValues([
|
|
15079
|
+
["alias", alias],
|
|
15080
|
+
["context", context],
|
|
15081
|
+
["principal", principalId],
|
|
15082
|
+
["role", role],
|
|
15083
|
+
["key id", keyId]
|
|
15084
|
+
]) + `
|
|
15085
|
+
|
|
15086
|
+
Now active. Use \`vectros switch ${alias}\` to return to it, \`vectros keyring show --format env\` to export the key for the MCP server.
|
|
15087
|
+
`
|
|
15088
|
+
);
|
|
15089
|
+
}
|
|
15090
|
+
|
|
15091
|
+
// src/commands/keyring.ts
|
|
15092
|
+
async function runKeyringList(opts, deps = {}) {
|
|
15093
|
+
const keyring = deps.keyring ?? new Keyring();
|
|
15094
|
+
try {
|
|
15095
|
+
const entries = await keyring.list();
|
|
15096
|
+
const active = await keyring.activeAlias();
|
|
15097
|
+
if (opts.json) {
|
|
15098
|
+
process.stdout.write(`${formatJson({ active, entries })}
|
|
15099
|
+
`);
|
|
15100
|
+
return 0;
|
|
15101
|
+
}
|
|
15102
|
+
if (entries.length === 0) {
|
|
15103
|
+
process.stdout.write("\n (no keyring entries yet \u2014 run `vectros join <context>` to add one)\n");
|
|
15104
|
+
return 0;
|
|
15105
|
+
}
|
|
15106
|
+
process.stdout.write(
|
|
15107
|
+
"\n" + formatTable(entries, [
|
|
15108
|
+
{ header: "", get: (e) => e.alias === active ? "*" : "" },
|
|
15109
|
+
{ header: "alias", get: (e) => e.alias },
|
|
15110
|
+
{ header: "context", get: (e) => e.context },
|
|
15111
|
+
{ header: "principal", get: (e) => e.principalId },
|
|
15112
|
+
{ header: "role", get: (e) => e.role },
|
|
15113
|
+
{ header: "env", get: (e) => e.env },
|
|
15114
|
+
{ header: "key id", get: (e) => e.keyId }
|
|
15115
|
+
]) + "\n"
|
|
15116
|
+
);
|
|
15117
|
+
return 0;
|
|
15118
|
+
} catch (err) {
|
|
15119
|
+
process.stderr.write(` \u2716 keyring list failed: ${formatError(err)}
|
|
15120
|
+
`);
|
|
15121
|
+
return 1;
|
|
15122
|
+
}
|
|
15123
|
+
}
|
|
15124
|
+
async function runKeyringUse(alias, opts, deps = {}) {
|
|
15125
|
+
const keyring = deps.keyring ?? new Keyring();
|
|
15126
|
+
try {
|
|
15127
|
+
await keyring.setActive(alias);
|
|
15128
|
+
if (opts.json) {
|
|
15129
|
+
process.stdout.write(`${formatJson({ active: alias })}
|
|
15130
|
+
`);
|
|
15131
|
+
return 0;
|
|
15132
|
+
}
|
|
15133
|
+
process.stdout.write(`
|
|
15134
|
+
\u2714 Active identity is now '${alias}'.
|
|
15135
|
+
`);
|
|
15136
|
+
return 0;
|
|
15137
|
+
} catch (err) {
|
|
15138
|
+
process.stderr.write(` \u2716 ${formatError(err)}
|
|
15139
|
+
`);
|
|
15140
|
+
return 2;
|
|
15141
|
+
}
|
|
15142
|
+
}
|
|
15143
|
+
async function runKeyringRemove(alias, opts, deps = {}) {
|
|
15144
|
+
const keyring = deps.keyring ?? new Keyring();
|
|
15145
|
+
try {
|
|
15146
|
+
const removed = await keyring.remove(alias);
|
|
15147
|
+
if (!removed) {
|
|
15148
|
+
process.stderr.write(` \u2716 No keyring entry named '${alias}'.
|
|
15149
|
+
`);
|
|
15150
|
+
return 2;
|
|
15151
|
+
}
|
|
15152
|
+
if (opts.json) {
|
|
15153
|
+
process.stdout.write(`${formatJson({ removed: alias })}
|
|
15154
|
+
`);
|
|
15155
|
+
return 0;
|
|
15156
|
+
}
|
|
15157
|
+
process.stdout.write(`
|
|
15158
|
+
\u2714 Removed '${alias}' from the keyring.
|
|
15159
|
+
`);
|
|
15160
|
+
return 0;
|
|
15161
|
+
} catch (err) {
|
|
15162
|
+
process.stderr.write(` \u2716 keyring remove failed: ${formatError(err)}
|
|
15163
|
+
`);
|
|
15164
|
+
return 1;
|
|
15165
|
+
}
|
|
15166
|
+
}
|
|
15167
|
+
async function runKeyringShow(opts, deps = {}) {
|
|
15168
|
+
const keyring = deps.keyring ?? new Keyring();
|
|
15169
|
+
try {
|
|
15170
|
+
const entry = opts.alias ? await keyring.get(opts.alias) : await keyring.getActive();
|
|
15171
|
+
if (!entry) {
|
|
15172
|
+
process.stderr.write(
|
|
15173
|
+
opts.alias ? ` \u2716 No keyring entry named '${opts.alias}'.
|
|
15174
|
+
` : " \u2716 No active identity. Run `vectros join <context>` or `vectros switch <alias>`.\n"
|
|
15175
|
+
);
|
|
15176
|
+
return 2;
|
|
15177
|
+
}
|
|
15178
|
+
const secret = await keyring.resolveSecret(entry.alias);
|
|
15179
|
+
if (!secret) {
|
|
15180
|
+
process.stderr.write(
|
|
15181
|
+
` \u2716 The stored secret for '${entry.alias}' is missing or unreadable. Re-issue it with \`vectros join ${entry.context} --alias ${entry.alias}\`.
|
|
15182
|
+
`
|
|
15183
|
+
);
|
|
15184
|
+
return 1;
|
|
15185
|
+
}
|
|
15186
|
+
switch (opts.format) {
|
|
15187
|
+
case "raw":
|
|
15188
|
+
process.stdout.write(`${secret}
|
|
15189
|
+
`);
|
|
15190
|
+
return 0;
|
|
15191
|
+
case "env":
|
|
15192
|
+
process.stdout.write(`VECTROS_API_KEY=${secret}
|
|
15193
|
+
`);
|
|
15194
|
+
return 0;
|
|
15195
|
+
case "human":
|
|
15196
|
+
process.stdout.write(
|
|
15197
|
+
"\n" + formatKeyValues([
|
|
15198
|
+
["alias", entry.alias],
|
|
15199
|
+
["context", entry.context],
|
|
15200
|
+
["principal", entry.principalId],
|
|
15201
|
+
["role", entry.role],
|
|
15202
|
+
["env", entry.env],
|
|
15203
|
+
["key id", entry.keyId]
|
|
15204
|
+
]) + `
|
|
15205
|
+
|
|
15206
|
+
VECTROS_API_KEY=${secret}
|
|
15207
|
+
\u26A0 This is a live credential \u2014 treat it like a password.
|
|
15208
|
+
`
|
|
15209
|
+
);
|
|
15210
|
+
return 0;
|
|
15211
|
+
}
|
|
15212
|
+
} catch (err) {
|
|
15213
|
+
process.stderr.write(` \u2716 keyring show failed: ${formatError(err)}
|
|
15214
|
+
`);
|
|
15215
|
+
return 1;
|
|
15216
|
+
}
|
|
15217
|
+
}
|
|
15218
|
+
|
|
14092
15219
|
// src/build-info.ts
|
|
14093
15220
|
var BUILD_INFO = {
|
|
14094
|
-
cli: true ? "0.
|
|
14095
|
-
sdk: true ? "0.
|
|
14096
|
-
blueprints: true ? "0.
|
|
15221
|
+
cli: true ? "0.9.0" : "dev",
|
|
15222
|
+
sdk: true ? "0.34.0" : "dev",
|
|
15223
|
+
blueprints: true ? "0.7.0" : "dev"
|
|
14097
15224
|
};
|
|
14098
15225
|
function formatBuildInfo() {
|
|
14099
15226
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|
|
@@ -14163,26 +15290,134 @@ function buildProgram(deps = {}) {
|
|
|
14163
15290
|
registerAccessCommands(program, deps);
|
|
14164
15291
|
registerRoleCommands(program, deps);
|
|
14165
15292
|
registerContextCommands(program, deps);
|
|
15293
|
+
registerJoinCommand(program, deps);
|
|
15294
|
+
registerKeyringCommands(program);
|
|
14166
15295
|
return program;
|
|
14167
15296
|
}
|
|
15297
|
+
function registerJoinCommand(program, deps) {
|
|
15298
|
+
const join4 = program.command("join").description("Enroll a principal in a context (grant + key) and keep the key in the keyring.").argument("<context>", "the app context to join").option("--role <roleId>", "reference a Role to grant (XOR --actions)").option("--actions <csv>", "inline single-clause actions to grant, comma-separated (XOR --role)").option("--as <principal>", "who to enroll: 'me' (default) or an explicit 'usr_<id>'", "me").option("--org <orgId>", "identity override: the org the member acts under (enables org/team-scoped writes)").option("--alias <name>", "keyring entry name (defaults to the context id)").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
|
|
15299
|
+
addAuthOptions(join4).action(
|
|
15300
|
+
async (contextArg, flags) => {
|
|
15301
|
+
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
15302
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
15303
|
+
`);
|
|
15304
|
+
process.exit(2);
|
|
15305
|
+
}
|
|
15306
|
+
process.exit(
|
|
15307
|
+
await runJoin(
|
|
15308
|
+
{
|
|
15309
|
+
...toCommonOptions(flags),
|
|
15310
|
+
contextArg,
|
|
15311
|
+
role: flags.role,
|
|
15312
|
+
actions: flags.actions,
|
|
15313
|
+
as: flags.as,
|
|
15314
|
+
org: flags.org,
|
|
15315
|
+
alias: flags.alias,
|
|
15316
|
+
tenant: flags.tenant
|
|
15317
|
+
},
|
|
15318
|
+
deps
|
|
15319
|
+
)
|
|
15320
|
+
);
|
|
15321
|
+
}
|
|
15322
|
+
);
|
|
15323
|
+
}
|
|
15324
|
+
function registerKeyringCommands(program) {
|
|
15325
|
+
const KEYRING_SHOW_FORMATS = ["human", "env", "raw"];
|
|
15326
|
+
const keyring = program.command("keyring").description("Inspect and manage the local multi-principal keyring.");
|
|
15327
|
+
keyring.command("list").description("List keyring entries and which one is active.").option("--json", "emit machine-readable JSON").action(async (flags) => {
|
|
15328
|
+
process.exit(await runKeyringList({ json: Boolean(flags.json) }));
|
|
15329
|
+
});
|
|
15330
|
+
keyring.command("use").description("Make an entry the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
15331
|
+
process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
|
|
15332
|
+
});
|
|
15333
|
+
keyring.command("show").description("Reveal the active (or a named) entry\u2019s key.").option("--alias <name>", "show this entry instead of the active one").addOption(new import_commander.Option("--format <fmt>", "how to emit the key").choices(KEYRING_SHOW_FORMATS).default("human")).action(async (flags) => {
|
|
15334
|
+
process.exit(await runKeyringShow({ alias: flags.alias, format: flags.format }));
|
|
15335
|
+
});
|
|
15336
|
+
keyring.command("remove").description("Drop an entry and its stored secret.").argument("<alias>", "the keyring entry to remove").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
15337
|
+
process.exit(await runKeyringRemove(alias, { json: Boolean(flags.json) }));
|
|
15338
|
+
});
|
|
15339
|
+
program.command("switch").description("Shortcut for `keyring use` \u2014 flip the active identity.").argument("<alias>", "the keyring entry to activate").option("--json", "emit machine-readable JSON").action(async (alias, flags) => {
|
|
15340
|
+
process.exit(await runKeyringUse(alias, { json: Boolean(flags.json) }));
|
|
15341
|
+
});
|
|
15342
|
+
}
|
|
14168
15343
|
function registerContextCommands(program, deps) {
|
|
14169
15344
|
const context = program.command("context").description("Manage app contexts (the namespace all data lives in).");
|
|
14170
|
-
const create = context.command("create").description("Create (idempotent) an app context.").argument("<contextId>", "context id (3-31 chars; lowercase letter + lowercase/digit/dash)").option("--name <name>", "human-readable name");
|
|
14171
|
-
addCommonAuthOptions(create).action(
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
15345
|
+
const create = context.command("create").description("Create (idempotent) an app context.").argument("<contextId>", "context id (3-31 chars; lowercase letter + lowercase/digit/dash)").option("--name <name>", "human-readable name").option("--tenant <tenant>", "tenant to create the context in: 'test' or 'live' (default live)");
|
|
15346
|
+
addCommonAuthOptions(create).action(
|
|
15347
|
+
async (contextId, flags) => {
|
|
15348
|
+
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
15349
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
15350
|
+
`);
|
|
15351
|
+
process.exit(2);
|
|
15352
|
+
}
|
|
15353
|
+
process.exit(
|
|
15354
|
+
await runContextCreate(
|
|
15355
|
+
{
|
|
15356
|
+
...toCommonOptions(flags),
|
|
15357
|
+
contextId,
|
|
15358
|
+
name: flags.name,
|
|
15359
|
+
tenant: flags.tenant
|
|
15360
|
+
},
|
|
15361
|
+
deps
|
|
15362
|
+
)
|
|
15363
|
+
);
|
|
15364
|
+
}
|
|
15365
|
+
);
|
|
15366
|
+
const list = context.command("list").description("List the tenant\u2019s app contexts.").option("--tenant <tenant>", "tenant to list: 'test' or 'live' (default live)");
|
|
14175
15367
|
addCommonAuthOptions(list).action(async (flags) => {
|
|
14176
|
-
|
|
15368
|
+
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
15369
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
15370
|
+
`);
|
|
15371
|
+
process.exit(2);
|
|
15372
|
+
}
|
|
15373
|
+
process.exit(
|
|
15374
|
+
await runContextList({ ...toCommonOptions(flags), tenant: flags.tenant }, deps)
|
|
15375
|
+
);
|
|
14177
15376
|
});
|
|
14178
|
-
const get = context.command("get").description("Show one app context.").argument("<contextId>");
|
|
15377
|
+
const get = context.command("get").description("Show one app context.").argument("<contextId>").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
14179
15378
|
addCommonAuthOptions(get).action(async (contextId, flags) => {
|
|
14180
|
-
|
|
15379
|
+
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
15380
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
15381
|
+
`);
|
|
15382
|
+
process.exit(2);
|
|
15383
|
+
}
|
|
15384
|
+
process.exit(
|
|
15385
|
+
await runContextGet(
|
|
15386
|
+
{ ...toCommonOptions(flags), contextId, tenant: flags.tenant },
|
|
15387
|
+
deps
|
|
15388
|
+
)
|
|
15389
|
+
);
|
|
14181
15390
|
});
|
|
15391
|
+
const destroy = context.command("destroy").description(
|
|
15392
|
+
"Permanently destroy an app context and ALL of its data (irreversible; asks for confirmation)."
|
|
15393
|
+
).argument("<contextId>", "the app context to destroy").option("--force", "skip the interactive confirmation (required when stdin is not a TTY)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
|
|
15394
|
+
addCommonAuthOptions(destroy).action(
|
|
15395
|
+
async (contextId, flags) => {
|
|
15396
|
+
if (flags.tenant !== void 0 && flags.tenant !== "test" && flags.tenant !== "live") {
|
|
15397
|
+
process.stderr.write(` \u2716 --tenant must be 'test' or 'live' (got '${flags.tenant}')
|
|
15398
|
+
`);
|
|
15399
|
+
process.exit(2);
|
|
15400
|
+
}
|
|
15401
|
+
process.exit(
|
|
15402
|
+
await runContextDestroy(
|
|
15403
|
+
{
|
|
15404
|
+
...toCommonOptions(flags),
|
|
15405
|
+
contextId,
|
|
15406
|
+
force: Boolean(flags.force),
|
|
15407
|
+
tenant: flags.tenant
|
|
15408
|
+
},
|
|
15409
|
+
deps
|
|
15410
|
+
)
|
|
15411
|
+
);
|
|
15412
|
+
}
|
|
15413
|
+
);
|
|
14182
15414
|
}
|
|
14183
15415
|
function registerRoleCommands(program, deps) {
|
|
14184
15416
|
const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
|
|
14185
|
-
const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
|
|
15417
|
+
const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
|
|
15418
|
+
"--data-scope <json>",
|
|
15419
|
+
`confine the clause to owned data \u2014 JSON map of dimension \u2192 allowed values, e.g. '{"userId":["\${{ self.userId }}"]}' or '{"scope:org":["<orgId>", null]}'`
|
|
15420
|
+
).option("--description <text>", "free-text description");
|
|
14186
15421
|
addCommonAuthOptions(create).action(
|
|
14187
15422
|
async (flags) => {
|
|
14188
15423
|
process.exit(
|
|
@@ -14192,6 +15427,7 @@ function registerRoleCommands(program, deps) {
|
|
|
14192
15427
|
roleId: flags.roleId,
|
|
14193
15428
|
name: flags.name,
|
|
14194
15429
|
actions: flags.actions,
|
|
15430
|
+
dataScope: flags.dataScope,
|
|
14195
15431
|
description: flags.description
|
|
14196
15432
|
}, deps)
|
|
14197
15433
|
);
|
|
@@ -14212,7 +15448,7 @@ function registerRoleCommands(program, deps) {
|
|
|
14212
15448
|
}
|
|
14213
15449
|
function registerAccessCommands(program, deps) {
|
|
14214
15450
|
const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
|
|
14215
|
-
const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)");
|
|
15451
|
+
const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (XOR --actions)").option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--org <orgId>", "identity override: the org this principal acts under (enables org/team-scoped writes)").option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --org)");
|
|
14216
15452
|
addCommonAuthOptions(grant).action(
|
|
14217
15453
|
async (flags) => {
|
|
14218
15454
|
process.exit(
|
|
@@ -14221,7 +15457,9 @@ function registerAccessCommands(program, deps) {
|
|
|
14221
15457
|
principal: flags.principal,
|
|
14222
15458
|
context: flags.context,
|
|
14223
15459
|
role: flags.role,
|
|
14224
|
-
actions: flags.actions
|
|
15460
|
+
actions: flags.actions,
|
|
15461
|
+
org: flags.org,
|
|
15462
|
+
identityOverrides: flags.identityOverrides
|
|
14225
15463
|
}, deps)
|
|
14226
15464
|
);
|
|
14227
15465
|
}
|
|
@@ -14238,6 +15476,12 @@ function registerAccessCommands(program, deps) {
|
|
|
14238
15476
|
addCommonAuthOptions(get).action(async (flags) => {
|
|
14239
15477
|
process.exit(await runAccessGet({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
|
|
14240
15478
|
});
|
|
15479
|
+
const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context");
|
|
15480
|
+
addCommonAuthOptions(explain).action(async (flags) => {
|
|
15481
|
+
process.exit(
|
|
15482
|
+
await runAccessExplain({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps)
|
|
15483
|
+
);
|
|
15484
|
+
});
|
|
14241
15485
|
}
|
|
14242
15486
|
function dimensionOption() {
|
|
14243
15487
|
return new import_commander.Option("--type <dim>", "principal dimension").choices(DIMENSIONS).makeOptionMandatory();
|