@vectros-ai/cli 0.12.0 → 0.13.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/AUTHORING.md +55 -7
- package/CHANGELOG.md +86 -0
- package/dist/cli.js +444 -72
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +444 -72
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -4043,7 +4043,7 @@ var AuthClient = class {
|
|
|
4043
4043
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/access-log");
|
|
4044
4044
|
}
|
|
4045
4045
|
/**
|
|
4046
|
-
* Lists
|
|
4046
|
+
* Lists your scoped API keys (`ssk_*`) in your credential's own environment — a live key lists live keys, a test key lists test keys. Revoked keys are excluded. Requires the `keys:r` scope.
|
|
4047
4047
|
*
|
|
4048
4048
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
4049
4049
|
*
|
|
@@ -4426,7 +4426,7 @@ var AuthClient = class {
|
|
|
4426
4426
|
);
|
|
4427
4427
|
}
|
|
4428
4428
|
/**
|
|
4429
|
-
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4429
|
+
* Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
|
|
4430
4430
|
*
|
|
4431
4431
|
* @param {Vectros.CreateAccessProfileRequest} request
|
|
4432
4432
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4844,7 +4844,7 @@ var AuthClient = class {
|
|
|
4844
4844
|
);
|
|
4845
4845
|
}
|
|
4846
4846
|
/**
|
|
4847
|
-
* Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own
|
|
4847
|
+
* Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
|
|
4848
4848
|
*
|
|
4849
4849
|
* @param {Vectros.UpdateAccessProfileRequest} request
|
|
4850
4850
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -4917,7 +4917,7 @@ var AuthClient = class {
|
|
|
4917
4917
|
);
|
|
4918
4918
|
}
|
|
4919
4919
|
/**
|
|
4920
|
-
* Deletes an access profile. Within about five minutes (the access-profile cache lifetime), token minting for this principal in this context will be denied. Requires the `profiles:d` scope.
|
|
4920
|
+
* Deletes an access profile. Within about five minutes (the access-profile cache lifetime), token minting for this principal in this context will be denied. If you use a scoped credential and the profile carries `identityOverrides`, you may only delete it when you hold those values yourself — deleting a profile removes its identity, so the same bound applies as when clearing it. A profile with no `identityOverrides` is unaffected, and a root API key (`sk_`) is exempt. Requires the `profiles:d` scope.
|
|
4921
4921
|
*
|
|
4922
4922
|
* @param {Vectros.DeleteAccessProfileRequest} request
|
|
4923
4923
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5771,7 +5771,7 @@ var AuthClient = class {
|
|
|
5771
5771
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
|
|
5772
5772
|
}
|
|
5773
5773
|
/**
|
|
5774
|
-
* Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `
|
|
5774
|
+
* Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
|
|
5775
5775
|
*
|
|
5776
5776
|
* @param {Vectros.CreateInviteRequest} request
|
|
5777
5777
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -5842,7 +5842,7 @@ var AuthClient = class {
|
|
|
5842
5842
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
|
|
5843
5843
|
}
|
|
5844
5844
|
/**
|
|
5845
|
-
* Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged.
|
|
5845
|
+
* Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
|
|
5846
5846
|
*
|
|
5847
5847
|
* @param {Vectros.CreateInviteRequest} request
|
|
5848
5848
|
* @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -6176,7 +6176,7 @@ var DocumentsClient = class {
|
|
|
6176
6176
|
* userId: "550e8400-e29b-41d4-a716-446655440000",
|
|
6177
6177
|
* scope: "group:eng-team",
|
|
6178
6178
|
* folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
6179
|
-
* startFrom: "
|
|
6179
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
6180
6180
|
* })
|
|
6181
6181
|
*/
|
|
6182
6182
|
listDocuments(request = {}, requestOptions) {
|
|
@@ -6486,6 +6486,7 @@ var DocumentsClient = class {
|
|
|
6486
6486
|
* @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
6487
6487
|
*
|
|
6488
6488
|
* @throws {@link Vectros.BadRequestError}
|
|
6489
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
6489
6490
|
* @throws {@link Vectros.NotFoundError}
|
|
6490
6491
|
* @throws {@link Vectros.ConflictError}
|
|
6491
6492
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -6533,6 +6534,8 @@ var DocumentsClient = class {
|
|
|
6533
6534
|
switch (_response.error.statusCode) {
|
|
6534
6535
|
case 400:
|
|
6535
6536
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
6537
|
+
case 403:
|
|
6538
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
6536
6539
|
case 404:
|
|
6537
6540
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
6538
6541
|
case 409:
|
|
@@ -6563,7 +6566,7 @@ var DocumentsClient = class {
|
|
|
6563
6566
|
* field: "po_number",
|
|
6564
6567
|
* value: "PO-1001",
|
|
6565
6568
|
* prefix: "PO-2024",
|
|
6566
|
-
* startFrom: "
|
|
6569
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
|
|
6567
6570
|
* userId: "550e8400-e29b-41d4-a716-446655440000",
|
|
6568
6571
|
* scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
6569
6572
|
* })
|
|
@@ -6939,7 +6942,7 @@ var FoldersClient = class {
|
|
|
6939
6942
|
* parentFolderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
6940
6943
|
* userId: "550e8400-e29b-41d4-a716-446655440000",
|
|
6941
6944
|
* scope: "group:eng-team",
|
|
6942
|
-
* startFrom: "
|
|
6945
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
6943
6946
|
* })
|
|
6944
6947
|
*/
|
|
6945
6948
|
listFolders(request = {}, requestOptions) {
|
|
@@ -7116,6 +7119,7 @@ var FoldersClient = class {
|
|
|
7116
7119
|
* @param {Vectros.UpdateFolderRequest} request
|
|
7117
7120
|
* @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
7118
7121
|
*
|
|
7122
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
7119
7123
|
* @throws {@link Vectros.NotFoundError}
|
|
7120
7124
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
7121
7125
|
*
|
|
@@ -7160,6 +7164,8 @@ var FoldersClient = class {
|
|
|
7160
7164
|
}
|
|
7161
7165
|
if (_response.error.reason === "status-code") {
|
|
7162
7166
|
switch (_response.error.statusCode) {
|
|
7167
|
+
case 403:
|
|
7168
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
7163
7169
|
case 404:
|
|
7164
7170
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
7165
7171
|
case 429:
|
|
@@ -7242,6 +7248,7 @@ var FoldersClient = class {
|
|
|
7242
7248
|
* @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
7243
7249
|
*
|
|
7244
7250
|
* @throws {@link Vectros.BadRequestError}
|
|
7251
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
7245
7252
|
* @throws {@link Vectros.NotFoundError}
|
|
7246
7253
|
* @throws {@link Vectros.ConflictError}
|
|
7247
7254
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -7289,6 +7296,8 @@ var FoldersClient = class {
|
|
|
7289
7296
|
switch (_response.error.statusCode) {
|
|
7290
7297
|
case 400:
|
|
7291
7298
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
7299
|
+
case 403:
|
|
7300
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
7292
7301
|
case 404:
|
|
7293
7302
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
7294
7303
|
case 409:
|
|
@@ -7376,7 +7385,8 @@ var IdentityClient = class {
|
|
|
7376
7385
|
*
|
|
7377
7386
|
* @example
|
|
7378
7387
|
* await client.identity.listEntities({
|
|
7379
|
-
* namespace: "team"
|
|
7388
|
+
* namespace: "team",
|
|
7389
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
7380
7390
|
* })
|
|
7381
7391
|
*/
|
|
7382
7392
|
listEntities(request, requestOptions) {
|
|
@@ -8140,7 +8150,7 @@ var IdentityClient = class {
|
|
|
8140
8150
|
* @example
|
|
8141
8151
|
* await client.identity.listUsers({
|
|
8142
8152
|
* externalId: "usr_12345",
|
|
8143
|
-
* startFrom: "
|
|
8153
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
|
|
8144
8154
|
* type: "person_v1",
|
|
8145
8155
|
* field: "team",
|
|
8146
8156
|
* value: "engineering",
|
|
@@ -8321,11 +8331,12 @@ var IdentityClient = class {
|
|
|
8321
8331
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
|
|
8322
8332
|
}
|
|
8323
8333
|
/**
|
|
8324
|
-
* Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
|
|
8334
|
+
* Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
|
|
8325
8335
|
*
|
|
8326
8336
|
* @param {Vectros.UpdateUserRequest} request
|
|
8327
8337
|
* @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
8328
8338
|
*
|
|
8339
|
+
* @throws {@link Vectros.BadRequestError}
|
|
8329
8340
|
* @throws {@link Vectros.NotFoundError}
|
|
8330
8341
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
8331
8342
|
*
|
|
@@ -8370,6 +8381,8 @@ var IdentityClient = class {
|
|
|
8370
8381
|
}
|
|
8371
8382
|
if (_response.error.reason === "status-code") {
|
|
8372
8383
|
switch (_response.error.statusCode) {
|
|
8384
|
+
case 400:
|
|
8385
|
+
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
8373
8386
|
case 404:
|
|
8374
8387
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
8375
8388
|
case 429:
|
|
@@ -9017,7 +9030,7 @@ var RecordsClient = class {
|
|
|
9017
9030
|
* folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
|
9018
9031
|
* userId: "550e8400-e29b-41d4-a716-446655440000",
|
|
9019
9032
|
* scope: "group:eng-team",
|
|
9020
|
-
* startFrom: "
|
|
9033
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
9021
9034
|
* })
|
|
9022
9035
|
*/
|
|
9023
9036
|
listRecords(request = {}, requestOptions) {
|
|
@@ -9205,6 +9218,7 @@ var RecordsClient = class {
|
|
|
9205
9218
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
9206
9219
|
*
|
|
9207
9220
|
* @throws {@link Vectros.BadRequestError}
|
|
9221
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
9208
9222
|
* @throws {@link Vectros.NotFoundError}
|
|
9209
9223
|
* @throws {@link Vectros.ConflictError}
|
|
9210
9224
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -9253,6 +9267,8 @@ var RecordsClient = class {
|
|
|
9253
9267
|
switch (_response.error.statusCode) {
|
|
9254
9268
|
case 400:
|
|
9255
9269
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
9270
|
+
case 403:
|
|
9271
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
9256
9272
|
case 404:
|
|
9257
9273
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
9258
9274
|
case 409:
|
|
@@ -9334,6 +9350,7 @@ var RecordsClient = class {
|
|
|
9334
9350
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
9335
9351
|
*
|
|
9336
9352
|
* @throws {@link Vectros.BadRequestError}
|
|
9353
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
9337
9354
|
* @throws {@link Vectros.NotFoundError}
|
|
9338
9355
|
* @throws {@link Vectros.ConflictError}
|
|
9339
9356
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
@@ -9384,6 +9401,8 @@ var RecordsClient = class {
|
|
|
9384
9401
|
switch (_response.error.statusCode) {
|
|
9385
9402
|
case 400:
|
|
9386
9403
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
9404
|
+
case 403:
|
|
9405
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
9387
9406
|
case 404:
|
|
9388
9407
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
9389
9408
|
case 409:
|
|
@@ -9401,7 +9420,15 @@ var RecordsClient = class {
|
|
|
9401
9420
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/records/{id}");
|
|
9402
9421
|
}
|
|
9403
9422
|
/**
|
|
9404
|
-
* Finds records by the value of a lookup field declared on the type's schema. Provide exactly one lookup mode: `value` (exact match), `from`+`to` (inclusive range, ascending by value), or `prefix` (string fields only, ascending). Range and prefix lookups are not supported on a sensitive field, because its value is stored as a blind index and has no sortable order. An exact-`value` lookup on a sensitive field is also rejected on this GET endpoint — the value must not appear in the URL — so use the `POST /v1/records/lookup` body variant for sensitive fields.
|
|
9423
|
+
* Finds records by the value of a lookup field declared on the type's schema. Provide exactly one lookup mode: `value` (exact match), `from`+`to` (inclusive range, ascending by value), or `prefix` (string fields only, ascending). Range and prefix lookups are not supported on a sensitive field, because its value is stored as a blind index and has no sortable order. An exact-`value` lookup on a sensitive field is also rejected on this GET endpoint — the value must not appear in the URL — so use the `POST /v1/records/lookup` body variant for sensitive fields.
|
|
9424
|
+
*
|
|
9425
|
+
* A `value` lookup can additionally be narrowed to a window of the lookup field's **sort key** using `sortFrom` and/or `sortTo` (inclusive) — for example, one session's records created since a timestamp. The sort key is whatever the schema declares as that lookup's `sortBy` (`createdAt` by default, `lastUpdated`, or another field), and bounds are given in that field's own units — epoch milliseconds for the two timestamp options. **Records that have no value for the sorted field are never included in a bounded window** — they are ordered ahead of every record that does have one, and a `sortFrom`/`sortTo` window only ever selects from records carrying a value. The sorted field does not have to be `required`.
|
|
9426
|
+
*
|
|
9427
|
+
* Narrowing is available on any lookup field your schema declares for fast equality lookup, and on `externalId` — which is always ordered by creation time, so its bounds are epoch milliseconds whatever the schema says. It is rejected (`400`) for a field declared with `rangeEnabled`, for a field declared beyond the schema's fast-lookup budget, for a lookup whose `sortBy` names a sensitive field (a sensitive value is stored as a blind index and has no order), and for a window whose start is after its end. Ownership fields are not lookup fields on this endpoint at all — see the `field` parameter.
|
|
9428
|
+
*
|
|
9429
|
+
* While paging a narrowed lookup, keep every other parameter identical. The cursor is valid only for the exact query that returned it — changing `order`, `sortFrom`, `sortTo`, or dropping them altogether, is rejected rather than silently resumed at a position that means something different in the new query.
|
|
9430
|
+
*
|
|
9431
|
+
* Results are paginated: set `limit` for the page size and pass the returned `nextCursor` back as `startFrom` for the next page. **Keep paging until `nextCursor` is null** — a page can come back empty or shorter than `limit` while more results remain, so an empty page is not the end of the results. Requires the `records:r:<type>` scope.
|
|
9405
9432
|
*
|
|
9406
9433
|
* @param {Vectros.LookupRecordsRequest} request
|
|
9407
9434
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -9413,22 +9440,40 @@ var RecordsClient = class {
|
|
|
9413
9440
|
* type: "intake_form",
|
|
9414
9441
|
* field: "email",
|
|
9415
9442
|
* value: "jane@example.com",
|
|
9443
|
+
* values: ["open"],
|
|
9416
9444
|
* prefix: "jane",
|
|
9417
|
-
* startFrom: "
|
|
9445
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
9418
9446
|
* })
|
|
9419
9447
|
*/
|
|
9420
9448
|
lookupRecords(request, requestOptions) {
|
|
9421
9449
|
return HttpResponsePromise.fromPromise(this.__lookupRecords(request, requestOptions));
|
|
9422
9450
|
}
|
|
9423
9451
|
async __lookupRecords(request, requestOptions) {
|
|
9424
|
-
const {
|
|
9452
|
+
const {
|
|
9453
|
+
type: type_,
|
|
9454
|
+
field,
|
|
9455
|
+
value,
|
|
9456
|
+
values,
|
|
9457
|
+
from: from_,
|
|
9458
|
+
to,
|
|
9459
|
+
prefix,
|
|
9460
|
+
sortFrom,
|
|
9461
|
+
sortTo,
|
|
9462
|
+
startFrom,
|
|
9463
|
+
limit,
|
|
9464
|
+
includePayload,
|
|
9465
|
+
order
|
|
9466
|
+
} = request;
|
|
9425
9467
|
const _queryParams = {
|
|
9426
9468
|
type: type_,
|
|
9427
9469
|
field,
|
|
9428
9470
|
value,
|
|
9471
|
+
values,
|
|
9429
9472
|
from: from_,
|
|
9430
9473
|
to,
|
|
9431
9474
|
prefix,
|
|
9475
|
+
sortFrom,
|
|
9476
|
+
sortTo,
|
|
9432
9477
|
startFrom,
|
|
9433
9478
|
limit,
|
|
9434
9479
|
includePayload,
|
|
@@ -9472,7 +9517,7 @@ var RecordsClient = class {
|
|
|
9472
9517
|
return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records/lookup");
|
|
9473
9518
|
}
|
|
9474
9519
|
/**
|
|
9475
|
-
* Body-based equivalent of `GET /v1/records/lookup`. Use this when looking up by a sensitive field: the value travels in the request body (and is blind-indexed server-side) instead of in the URL query string, so it never lands in access, CDN, or proxy logs. The GET variant rejects an exact-value lookup on a sensitive field and directs you here. Non-sensitive exact-value, range (`from`+`to`), and prefix lookups also work here. Returns the same `{data, nextCursor}` envelope and uses the same pagination as the GET variant. Requires the `records:r:<type>` scope.
|
|
9520
|
+
* Body-based equivalent of `GET /v1/records/lookup`. Use this when looking up by a sensitive field: the value travels in the request body (and is blind-indexed server-side) instead of in the URL query string, so it never lands in access, CDN, or proxy logs. The GET variant rejects an exact-value lookup on a sensitive field and directs you here. Non-sensitive exact-value, range (`from`+`to`), and prefix lookups also work here, as does narrowing a `value` lookup by the field's sort key with `sortFrom`/`sortTo` — see the GET variant for what the sort key is and when narrowing by it is available. Returns the same `{data, nextCursor}` envelope and uses the same pagination as the GET variant; keep paging until `nextCursor` is null rather than stopping on an empty page. Requires the `records:r:<type>` scope.
|
|
9476
9521
|
*
|
|
9477
9522
|
* @param {Vectros.RecordLookupRequest} request
|
|
9478
9523
|
* @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
@@ -9667,7 +9712,7 @@ var SchemasClient = class {
|
|
|
9667
9712
|
* scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
9668
9713
|
* surface: "document",
|
|
9669
9714
|
* recordType: "intake_form",
|
|
9670
|
-
* startFrom: "
|
|
9715
|
+
* startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
|
|
9671
9716
|
* })
|
|
9672
9717
|
*/
|
|
9673
9718
|
listSchemas(request = {}, requestOptions) {
|
|
@@ -9871,6 +9916,7 @@ var SchemasClient = class {
|
|
|
9871
9916
|
* @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
|
|
9872
9917
|
*
|
|
9873
9918
|
* @throws {@link Vectros.BadRequestError}
|
|
9919
|
+
* @throws {@link Vectros.ForbiddenError}
|
|
9874
9920
|
* @throws {@link Vectros.NotFoundError}
|
|
9875
9921
|
* @throws {@link Vectros.TooManyRequestsError}
|
|
9876
9922
|
*
|
|
@@ -9919,6 +9965,8 @@ var SchemasClient = class {
|
|
|
9919
9965
|
switch (_response.error.statusCode) {
|
|
9920
9966
|
case 400:
|
|
9921
9967
|
throw new BadRequestError(_response.error.body, _response.rawResponse);
|
|
9968
|
+
case 403:
|
|
9969
|
+
throw new ForbiddenError(_response.error.body, _response.rawResponse);
|
|
9922
9970
|
case 404:
|
|
9923
9971
|
throw new NotFoundError(_response.error.body, _response.rawResponse);
|
|
9924
9972
|
case 429:
|
|
@@ -10185,7 +10233,7 @@ var logging;
|
|
|
10185
10233
|
logging2.ConsoleLogger = ConsoleLogger;
|
|
10186
10234
|
})(logging || (logging = {}));
|
|
10187
10235
|
|
|
10188
|
-
//
|
|
10236
|
+
// node_modules/@vectros-ai/blueprints/dist/index.mjs
|
|
10189
10237
|
var import_zod = require("zod");
|
|
10190
10238
|
var import_zod2 = require("zod");
|
|
10191
10239
|
var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
@@ -10289,19 +10337,63 @@ var BlueprintFieldDefSchema = import_zod.z.object({
|
|
|
10289
10337
|
var BlueprintLookupFieldSchema = import_zod.z.union([
|
|
10290
10338
|
import_zod.z.string().min(1),
|
|
10291
10339
|
import_zod.z.object({
|
|
10292
|
-
|
|
10340
|
+
// A lookup field declares either ONE field (fieldName) or 2-3 fields
|
|
10341
|
+
// matched together (fieldNames) — never both, never neither. `.strict()`
|
|
10342
|
+
// below only rejects unknown KEYS; it does not enforce this combination,
|
|
10343
|
+
// so the `.superRefine()` after it carries the actual rule.
|
|
10344
|
+
fieldName: import_zod.z.string().min(1).optional(),
|
|
10345
|
+
// Composite (conjunctive) lookup: match on all of these fields at once —
|
|
10346
|
+
// "every record where `status` is `open` AND `area` is `billing`", exact,
|
|
10347
|
+
// in a stable order. Order is significant and locked at create: a query
|
|
10348
|
+
// may match a leading run of the list (the first field alone, the first
|
|
10349
|
+
// two together, …) but never a later field alone — declare a separate
|
|
10350
|
+
// lookup for that. Record-only: the schema's `allowedSurfaces` must be
|
|
10351
|
+
// exactly `['record']` (checked at the schema level below) — the
|
|
10352
|
+
// platform's composite index has no document/user/entity reader yet.
|
|
10353
|
+
fieldNames: import_zod.z.array(import_zod.z.string().min(1)).min(2).max(3).optional(),
|
|
10354
|
+
// Not available on a composite (`fieldNames`) lookup — checked below.
|
|
10293
10355
|
unique: import_zod.z.boolean().optional(),
|
|
10294
10356
|
// Opt this field into ordered range + prefix lookups (from/to/prefix) on
|
|
10295
10357
|
// top of exact match. Billed at the range-index rate; not valid on a
|
|
10296
|
-
// sensitive field (a blind index is not orderable)
|
|
10358
|
+
// sensitive field (a blind index is not orderable), and — like `unique`
|
|
10359
|
+
// — not available on a composite lookup: it is an exact-match index over
|
|
10360
|
+
// its fields; declare the range lookup separately. Locked at create.
|
|
10297
10361
|
rangeEnabled: import_zod.z.boolean().optional(),
|
|
10298
10362
|
// Sort key for the exact-match index: 'createdAt' (default), 'lastUpdated',
|
|
10299
|
-
// or a declared field on this schema. Locked at create.
|
|
10363
|
+
// or a declared field on this schema. Locked at create. Valid on a
|
|
10364
|
+
// composite lookup too — it orders WITHIN a group when a query supplies
|
|
10365
|
+
// fewer values than the lookup declares, not across the whole result.
|
|
10300
10366
|
sortBy: import_zod.z.string().min(1).optional(),
|
|
10301
10367
|
// Opt a field past the fixed fast-index budget into a higher-cost
|
|
10302
10368
|
// secondary index. No effect on a field that fits within the budget.
|
|
10303
10369
|
allowOverflow: import_zod.z.boolean().optional()
|
|
10304
|
-
}).strict()
|
|
10370
|
+
}).strict().superRefine((lf, ctx) => {
|
|
10371
|
+
const hasFieldName = lf.fieldName !== void 0;
|
|
10372
|
+
const hasFieldNames = lf.fieldNames !== void 0;
|
|
10373
|
+
if (hasFieldName === hasFieldNames) {
|
|
10374
|
+
ctx.addIssue({
|
|
10375
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10376
|
+
message: "a lookup field declares either 'fieldName' (one field) or 'fieldNames' (2-3 fields matched together), never both and never neither"
|
|
10377
|
+
});
|
|
10378
|
+
return;
|
|
10379
|
+
}
|
|
10380
|
+
if (hasFieldNames) {
|
|
10381
|
+
if (lf.unique !== void 0) {
|
|
10382
|
+
ctx.addIssue({
|
|
10383
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10384
|
+
path: ["unique"],
|
|
10385
|
+
message: "'unique' is not available on a composite ('fieldNames') lookup"
|
|
10386
|
+
});
|
|
10387
|
+
}
|
|
10388
|
+
if (lf.rangeEnabled !== void 0) {
|
|
10389
|
+
ctx.addIssue({
|
|
10390
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10391
|
+
path: ["rangeEnabled"],
|
|
10392
|
+
message: "'rangeEnabled' is not available on a composite ('fieldNames') lookup"
|
|
10393
|
+
});
|
|
10394
|
+
}
|
|
10395
|
+
}
|
|
10396
|
+
})
|
|
10305
10397
|
]);
|
|
10306
10398
|
var BlueprintCapabilitiesSchema = import_zod.z.object({
|
|
10307
10399
|
auditHistory: import_zod.z.boolean().optional()
|
|
@@ -10339,7 +10431,21 @@ var BlueprintSchemaSchema = import_zod.z.object({
|
|
|
10339
10431
|
// hop); immutable once set. Mirrors the `basedOn` field on the platform's
|
|
10340
10432
|
// schema-create request.
|
|
10341
10433
|
basedOn: import_zod.z.string().min(1).optional()
|
|
10342
|
-
}).strict()
|
|
10434
|
+
}).strict().superRefine((schema, ctx) => {
|
|
10435
|
+
const hasComposite = (schema.lookupFields ?? []).some(
|
|
10436
|
+
(lf) => typeof lf === "object" && lf.fieldNames !== void 0
|
|
10437
|
+
);
|
|
10438
|
+
if (!hasComposite) return;
|
|
10439
|
+
const surfaces = schema.allowedSurfaces;
|
|
10440
|
+
const isRecordOnly = surfaces === void 0 || surfaces.length === 1 && surfaces[0] === "record";
|
|
10441
|
+
if (!isRecordOnly) {
|
|
10442
|
+
ctx.addIssue({
|
|
10443
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
10444
|
+
path: ["allowedSurfaces"],
|
|
10445
|
+
message: `schema '${schema.typeName}': a composite ('fieldNames') lookup requires allowedSurfaces to be exactly ['record'] \u2014 the platform's composite index has no document/user/entity reader yet`
|
|
10446
|
+
});
|
|
10447
|
+
}
|
|
10448
|
+
});
|
|
10343
10449
|
var BlueprintAccessProfileSchema = import_zod.z.object({
|
|
10344
10450
|
// Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
|
|
10345
10451
|
// what enforces the data-plane-only security boundary.
|
|
@@ -12278,9 +12384,10 @@ var secondBrain = {
|
|
|
12278
12384
|
// Lookup shapes are MIGRATION-LOCKED once the schema is live (a field cannot
|
|
12279
12385
|
// flip equality↔range later), so each is chosen on purpose:
|
|
12280
12386
|
// • `source` — EQUALITY, sorted by `lastUpdated`: enumerate notes from one
|
|
12281
|
-
// source, most-recently-touched first.
|
|
12282
|
-
//
|
|
12283
|
-
//
|
|
12387
|
+
// source, most-recently-touched first. A platform timestamp is the natural
|
|
12388
|
+
// choice for "most recently touched" and is always present, so every note
|
|
12389
|
+
// has a position. (Sorting by an OPTIONAL payload field is also fine — notes
|
|
12390
|
+
// lacking it are ordered ahead of those that have it, never dropped.)
|
|
12284
12391
|
// • `status` — EQUALITY: "show archived notes".
|
|
12285
12392
|
// • `capturedAt` — RANGE: ordered `from`/`to`/`prefix` ("captured in 2026-06").
|
|
12286
12393
|
// `externalId` has a built-in first-class finder — look it up directly, never
|
|
@@ -13402,7 +13509,16 @@ function toSdkFields(blueprint, schemaIndex) {
|
|
|
13402
13509
|
function toLookupDefs(lookupFields) {
|
|
13403
13510
|
return lookupFields?.map((lf) => {
|
|
13404
13511
|
if (typeof lf === "string") return { fieldName: lf };
|
|
13405
|
-
const
|
|
13512
|
+
const hasFieldName = lf.fieldName !== void 0;
|
|
13513
|
+
const hasFieldNames = lf.fieldNames !== void 0;
|
|
13514
|
+
if (hasFieldName === hasFieldNames) {
|
|
13515
|
+
throw new Error(
|
|
13516
|
+
`Internal error: lookupField resolved to ${hasFieldName ? "both fieldName and fieldNames" : "neither fieldName nor fieldNames"} \u2014 @vectros-ai/blueprints' parseBlueprint should have refused this before the loader ever saw it.`
|
|
13517
|
+
);
|
|
13518
|
+
}
|
|
13519
|
+
const def = {};
|
|
13520
|
+
if (lf.fieldName !== void 0) def.fieldName = lf.fieldName;
|
|
13521
|
+
if (lf.fieldNames !== void 0) def.fieldNames = lf.fieldNames;
|
|
13406
13522
|
if (lf.unique !== void 0) def.unique = lf.unique;
|
|
13407
13523
|
if (lf.rangeEnabled !== void 0) def.rangeEnabled = lf.rangeEnabled;
|
|
13408
13524
|
if (lf.sortBy !== void 0) def.sortBy = lf.sortBy;
|
|
@@ -13665,7 +13781,7 @@ function buildPlan(blueprint, opts = {}) {
|
|
|
13665
13781
|
summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
|
|
13666
13782
|
});
|
|
13667
13783
|
for (const s of blueprint.schemas) {
|
|
13668
|
-
const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldName) ?? [];
|
|
13784
|
+
const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
|
|
13669
13785
|
const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
|
|
13670
13786
|
const surfaceNote = s.allowedSurfaces ? ` surfaces: [${s.allowedSurfaces.join(", ")}]` : "";
|
|
13671
13787
|
const sensitive = s.fields.filter((f) => f.sensitive).map((f) => f.fieldId);
|
|
@@ -14257,10 +14373,121 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
|
|
|
14257
14373
|
// src/cli-blueprint.ts
|
|
14258
14374
|
var import_promises3 = require("fs/promises");
|
|
14259
14375
|
|
|
14376
|
+
// src/scope-grammar.ts
|
|
14377
|
+
var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
14378
|
+
"user",
|
|
14379
|
+
"record",
|
|
14380
|
+
"document",
|
|
14381
|
+
"entity",
|
|
14382
|
+
"self",
|
|
14383
|
+
"tenant",
|
|
14384
|
+
"context",
|
|
14385
|
+
"scope",
|
|
14386
|
+
"versions",
|
|
14387
|
+
"lookup"
|
|
14388
|
+
]);
|
|
14389
|
+
var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
14390
|
+
var SCOPE_NAMESPACE_MIN_LENGTH = 2;
|
|
14391
|
+
var SCOPE_NAMESPACE_MAX_LENGTH = 32;
|
|
14392
|
+
var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
14393
|
+
var SCOPE_VALUE_MIN_LENGTH = 1;
|
|
14394
|
+
var SCOPE_VALUE_MAX_LENGTH = 128;
|
|
14395
|
+
function valueGrammarError(value) {
|
|
14396
|
+
if (!SCOPE_VALUE_RE.test(value)) {
|
|
14397
|
+
return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
|
|
14398
|
+
}
|
|
14399
|
+
return null;
|
|
14400
|
+
}
|
|
14401
|
+
function namespaceGrammarError(ns) {
|
|
14402
|
+
if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
|
|
14403
|
+
return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
|
|
14404
|
+
}
|
|
14405
|
+
if (!SCOPE_NAMESPACE_RE.test(ns)) {
|
|
14406
|
+
return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
|
|
14407
|
+
}
|
|
14408
|
+
return null;
|
|
14409
|
+
}
|
|
14410
|
+
function assertScopeEntries(entries, flag) {
|
|
14411
|
+
if (!entries?.length) return;
|
|
14412
|
+
for (const raw of entries) {
|
|
14413
|
+
const sep = raw.indexOf(":");
|
|
14414
|
+
if (sep <= 0 || sep === raw.length - 1) {
|
|
14415
|
+
throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
|
|
14416
|
+
}
|
|
14417
|
+
const nsError = namespaceGrammarError(raw.slice(0, sep));
|
|
14418
|
+
if (nsError) throw new Error(`${flag} namespace ${nsError}`);
|
|
14419
|
+
const valError = valueGrammarError(raw.slice(sep + 1));
|
|
14420
|
+
if (valError) throw new Error(`${flag} value ${valError}`);
|
|
14421
|
+
}
|
|
14422
|
+
}
|
|
14423
|
+
function assertIdentityOverrideValues(overrides, flag) {
|
|
14424
|
+
if (!overrides) return;
|
|
14425
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
14426
|
+
if (!key.startsWith("scope:")) continue;
|
|
14427
|
+
const error = valueGrammarError(typeof value === "string" ? value : String(value));
|
|
14428
|
+
if (error) throw new Error(`${flag} '${key}' value ${error}`);
|
|
14429
|
+
}
|
|
14430
|
+
}
|
|
14431
|
+
function placeholderScopeNamespace(value) {
|
|
14432
|
+
const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
|
|
14433
|
+
return m ? m[1] : null;
|
|
14434
|
+
}
|
|
14435
|
+
var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
|
|
14436
|
+
var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
|
|
14437
|
+
|
|
14260
14438
|
// src/lint.ts
|
|
14261
14439
|
var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
|
|
14262
|
-
|
|
14263
|
-
|
|
14440
|
+
var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
|
|
14441
|
+
function lookupFieldNames(lf) {
|
|
14442
|
+
if (typeof lf === "string") return [lf];
|
|
14443
|
+
if (lf.fieldNames !== void 0) return lf.fieldNames;
|
|
14444
|
+
return lf.fieldName !== void 0 ? [lf.fieldName] : [];
|
|
14445
|
+
}
|
|
14446
|
+
function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
|
|
14447
|
+
const findings = [];
|
|
14448
|
+
const id = `[${lf.fieldNames.join(", ")}]`;
|
|
14449
|
+
const seenLegs = /* @__PURE__ */ new Set();
|
|
14450
|
+
for (const leg of lf.fieldNames) {
|
|
14451
|
+
if (seenLegs.has(leg)) {
|
|
14452
|
+
findings.push({
|
|
14453
|
+
severity: "error",
|
|
14454
|
+
rule: "lookup-composite-repeated-leg",
|
|
14455
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} repeats the leg '${leg}'. Each leg occupies its own position in the sort key, so a repeat doubles the key cost of every row to express a conjunction of a field with itself.`
|
|
14456
|
+
});
|
|
14457
|
+
}
|
|
14458
|
+
seenLegs.add(leg);
|
|
14459
|
+
const field = fieldsById.get(leg);
|
|
14460
|
+
if (!field) continue;
|
|
14461
|
+
if (field.fieldType === "array" || field.fieldType === "object") {
|
|
14462
|
+
findings.push({
|
|
14463
|
+
severity: "error",
|
|
14464
|
+
rule: "lookup-composite-leg-multi-valued",
|
|
14465
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is declared '${field.fieldType}'. A leg occupies one sort-key position and holds one value \u2014 a multi-valued or structured field would index only one rendering and the conjunction would return a subset while appearing complete. Use a scalar field as a leg.`
|
|
14466
|
+
});
|
|
14467
|
+
}
|
|
14468
|
+
if (field.fieldType === "reference" && field.cardinality === "many") {
|
|
14469
|
+
findings.push({
|
|
14470
|
+
severity: "error",
|
|
14471
|
+
rule: "lookup-composite-leg-multi-valued",
|
|
14472
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is a reference with cardinality 'many'. One sort-key position holds one value, so a multi-valued leg would index only one target and the conjunction would silently return a subset. Use a cardinality 'one' reference, or a scalar field.`
|
|
14473
|
+
});
|
|
14474
|
+
}
|
|
14475
|
+
if (rangeEnabledFieldNames.has(leg)) {
|
|
14476
|
+
findings.push({
|
|
14477
|
+
severity: "error",
|
|
14478
|
+
rule: "lookup-composite-leg-range-enabled",
|
|
14479
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but '${leg}' has its own lookupField declaring rangeEnabled \u2014 that stores it as an ordered row keyed on its own value, not an equality index. A leg is matched exactly. Declare the composite over non-range fields, and keep the range lookup as its own declaration.`
|
|
14480
|
+
});
|
|
14481
|
+
}
|
|
14482
|
+
if (lf.sortBy !== void 0 && leg === lf.sortBy) {
|
|
14483
|
+
findings.push({
|
|
14484
|
+
severity: "error",
|
|
14485
|
+
rule: "lookup-composite-leg-is-sortby",
|
|
14486
|
+
message: `schema '${schema.typeName}': composite lookupField ${id} cannot use '${leg}' as both a leg and its sortBy target. Within a fixed tuple every matching record has the same value for that leg, so the sort payload would be constant and a sortFrom/sortTo window would return either everything or nothing \u2014 at double the sort-key cost. Sort by a different field, 'createdAt', or 'lastUpdated'.`
|
|
14487
|
+
});
|
|
14488
|
+
}
|
|
14489
|
+
}
|
|
14490
|
+
return findings;
|
|
14264
14491
|
}
|
|
14265
14492
|
function lintBlueprint(bp) {
|
|
14266
14493
|
const findings = [];
|
|
@@ -14268,6 +14495,7 @@ function lintBlueprint(bp) {
|
|
|
14268
14495
|
for (const schema of bp.schemas) {
|
|
14269
14496
|
typeNames.add(schema.typeName);
|
|
14270
14497
|
const fieldIds = /* @__PURE__ */ new Set();
|
|
14498
|
+
const fieldsById = /* @__PURE__ */ new Map();
|
|
14271
14499
|
for (const f of schema.fields) {
|
|
14272
14500
|
if (fieldIds.has(f.fieldId)) {
|
|
14273
14501
|
findings.push({
|
|
@@ -14277,6 +14505,7 @@ function lintBlueprint(bp) {
|
|
|
14277
14505
|
});
|
|
14278
14506
|
}
|
|
14279
14507
|
fieldIds.add(f.fieldId);
|
|
14508
|
+
fieldsById.set(f.fieldId, f);
|
|
14280
14509
|
if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
|
|
14281
14510
|
findings.push({
|
|
14282
14511
|
severity: "warning",
|
|
@@ -14285,14 +14514,35 @@ function lintBlueprint(bp) {
|
|
|
14285
14514
|
});
|
|
14286
14515
|
}
|
|
14287
14516
|
}
|
|
14517
|
+
const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
|
|
14288
14518
|
for (const lf of schema.lookupFields ?? []) {
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14519
|
+
if (typeof lf !== "string" && lf.fieldName !== void 0 && lf.rangeEnabled) {
|
|
14520
|
+
rangeEnabledFieldNames.add(lf.fieldName);
|
|
14521
|
+
}
|
|
14522
|
+
}
|
|
14523
|
+
for (const lf of schema.lookupFields ?? []) {
|
|
14524
|
+
for (const name of lookupFieldNames(lf)) {
|
|
14525
|
+
if (!fieldIds.has(name)) {
|
|
14526
|
+
findings.push({
|
|
14527
|
+
severity: "error",
|
|
14528
|
+
rule: "lookup-field-unknown",
|
|
14529
|
+
message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
|
|
14530
|
+
});
|
|
14531
|
+
}
|
|
14532
|
+
}
|
|
14533
|
+
if (typeof lf !== "string" && lf.fieldNames !== void 0) {
|
|
14534
|
+
findings.push(
|
|
14535
|
+
...compositeLegFindings(schema, { fieldNames: lf.fieldNames, sortBy: lf.sortBy }, fieldsById, rangeEnabledFieldNames)
|
|
14536
|
+
);
|
|
14537
|
+
}
|
|
14538
|
+
if (typeof lf !== "string" && lf.sortBy !== void 0) {
|
|
14539
|
+
if (!BUILT_IN_SORT_BY.has(lf.sortBy) && !fieldIds.has(lf.sortBy)) {
|
|
14540
|
+
findings.push({
|
|
14541
|
+
severity: "warning",
|
|
14542
|
+
rule: "lookup-sortby-unknown-field",
|
|
14543
|
+
message: `schema '${schema.typeName}': lookupField sortBy '${lf.sortBy}' is not a declared field (nor 'createdAt'/'lastUpdated'). The platform does not reject this \u2014 it silently treats the sort target as an untyped field no record ever populates, which usually isn't what was intended. Likely a typo.`
|
|
14544
|
+
});
|
|
14545
|
+
}
|
|
14296
14546
|
}
|
|
14297
14547
|
}
|
|
14298
14548
|
}
|
|
@@ -14308,6 +14558,10 @@ function lintBlueprint(bp) {
|
|
|
14308
14558
|
const ds = bp.accessProfile.dataScope;
|
|
14309
14559
|
if (ds) {
|
|
14310
14560
|
for (const [key, values] of Object.entries(ds)) {
|
|
14561
|
+
for (const value of values) {
|
|
14562
|
+
const bad = placeholderFinding("accessProfile", key, value);
|
|
14563
|
+
if (bad) findings.push(bad);
|
|
14564
|
+
}
|
|
14311
14565
|
if (values.length > 0 && !values.includes(null)) {
|
|
14312
14566
|
findings.push({
|
|
14313
14567
|
severity: "warning",
|
|
@@ -14331,6 +14585,10 @@ function lintBlueprint(bp) {
|
|
|
14331
14585
|
const cds = clause.dataScope;
|
|
14332
14586
|
if (!cds) return;
|
|
14333
14587
|
for (const [key, values] of Object.entries(cds)) {
|
|
14588
|
+
for (const value of values) {
|
|
14589
|
+
const bad = placeholderFinding(`role '${roleId}' clause ${i}`, key, value);
|
|
14590
|
+
if (bad) findings.push(bad);
|
|
14591
|
+
}
|
|
14334
14592
|
if (values.length > 0 && !values.includes(null)) {
|
|
14335
14593
|
findings.push({
|
|
14336
14594
|
severity: "warning",
|
|
@@ -14344,6 +14602,28 @@ function lintBlueprint(bp) {
|
|
|
14344
14602
|
return findings;
|
|
14345
14603
|
}
|
|
14346
14604
|
var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
|
|
14605
|
+
var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
|
|
14606
|
+
var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
|
|
14607
|
+
var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
|
|
14608
|
+
function placeholderFinding(where, key, value) {
|
|
14609
|
+
if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
|
|
14610
|
+
if (BLUEPRINT_TOKEN_RE.test(value)) return null;
|
|
14611
|
+
if (RECOGNIZED_PLACEHOLDER_RES.some((re) => re.test(value))) {
|
|
14612
|
+
const ns = placeholderScopeNamespace(value);
|
|
14613
|
+
const nsError = ns === null ? null : namespaceGrammarError(ns);
|
|
14614
|
+
if (nsError === null) return null;
|
|
14615
|
+
return {
|
|
14616
|
+
severity: "error",
|
|
14617
|
+
rule: "datascope-unknown-placeholder",
|
|
14618
|
+
message: `${where}: dataScope.${key} value '${value}' names a namespace the platform rejects \u2014 ${nsError}. It will be rejected at apply.`
|
|
14619
|
+
};
|
|
14620
|
+
}
|
|
14621
|
+
return {
|
|
14622
|
+
severity: "error",
|
|
14623
|
+
rule: "datascope-unknown-placeholder",
|
|
14624
|
+
message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
|
|
14625
|
+
};
|
|
14626
|
+
}
|
|
14347
14627
|
function lintPasses(findings) {
|
|
14348
14628
|
return !findings.some((f) => f.severity === "error");
|
|
14349
14629
|
}
|
|
@@ -14747,11 +15027,14 @@ function parseScopeOverrides(scopes) {
|
|
|
14747
15027
|
throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
|
|
14748
15028
|
}
|
|
14749
15029
|
const ns = raw.slice(0, sep);
|
|
15030
|
+
const value = raw.slice(sep + 1);
|
|
15031
|
+
const valueError = valueGrammarError(value);
|
|
15032
|
+
if (valueError) throw new Error(`--scope value ${valueError}`);
|
|
14750
15033
|
const key = `scope:${ns}`;
|
|
14751
15034
|
if (Object.prototype.hasOwnProperty.call(out, key)) {
|
|
14752
15035
|
throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
|
|
14753
15036
|
}
|
|
14754
|
-
out[key] =
|
|
15037
|
+
out[key] = value;
|
|
14755
15038
|
}
|
|
14756
15039
|
if (Object.keys(out).length > 2) {
|
|
14757
15040
|
throw new Error("--scope allows at most two distinct namespaces.");
|
|
@@ -15096,6 +15379,7 @@ async function createIdentity(client, opts, payload) {
|
|
|
15096
15379
|
}
|
|
15097
15380
|
});
|
|
15098
15381
|
}
|
|
15382
|
+
assertScopeEntries(opts.scope, "--scope");
|
|
15099
15383
|
return client.identity.createEntity({
|
|
15100
15384
|
namespace: opts.type,
|
|
15101
15385
|
body: {
|
|
@@ -15301,6 +15585,7 @@ async function runAccessGrant(opts, deps = {}) {
|
|
|
15301
15585
|
...parseScopeOverrides(opts.scope) ?? {},
|
|
15302
15586
|
...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
|
|
15303
15587
|
};
|
|
15588
|
+
assertIdentityOverrideValues(identityOverrides, "--identity-overrides");
|
|
15304
15589
|
} catch (err) {
|
|
15305
15590
|
process.stderr.write(` \u2716 ${formatError(err)}
|
|
15306
15591
|
`);
|
|
@@ -15453,30 +15738,21 @@ ${renderProfile(p)}
|
|
|
15453
15738
|
|
|
15454
15739
|
// src/commands/access-explain.ts
|
|
15455
15740
|
var SELF = /* @__PURE__ */ Symbol("self");
|
|
15741
|
+
var ANY = /* @__PURE__ */ Symbol("any");
|
|
15742
|
+
var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
|
|
15456
15743
|
function canonicalDim(key) {
|
|
15457
15744
|
if (key === "userId" || key === "partnerUserId") return "user";
|
|
15458
15745
|
return key;
|
|
15459
15746
|
}
|
|
15460
|
-
var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
|
|
15461
|
-
"user",
|
|
15462
|
-
"record",
|
|
15463
|
-
"document",
|
|
15464
|
-
"entity",
|
|
15465
|
-
"self",
|
|
15466
|
-
"tenant",
|
|
15467
|
-
"context",
|
|
15468
|
-
"scope",
|
|
15469
|
-
"versions",
|
|
15470
|
-
"lookup"
|
|
15471
|
-
]);
|
|
15472
|
-
var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
15473
15747
|
function isRecognizedOwnershipKey(key) {
|
|
15748
|
+
if (key === DIM_WILDCARD) return true;
|
|
15474
15749
|
if (key === "userId" || key === "partnerUserId") return true;
|
|
15475
15750
|
if (!key.startsWith("scope:")) return false;
|
|
15476
15751
|
const ns = key.slice("scope:".length);
|
|
15477
15752
|
return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
|
|
15478
15753
|
}
|
|
15479
|
-
var
|
|
15754
|
+
var DIM_WILDCARD = "*";
|
|
15755
|
+
var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
|
|
15480
15756
|
function normalizeClauses(raw) {
|
|
15481
15757
|
const out = [];
|
|
15482
15758
|
for (const c of raw) {
|
|
@@ -15492,7 +15768,9 @@ function normalizeClauses(raw) {
|
|
|
15492
15768
|
dataScope[dim] = values.map((val) => {
|
|
15493
15769
|
if (val === null) return null;
|
|
15494
15770
|
const s = String(val);
|
|
15495
|
-
|
|
15771
|
+
if (ANY_TOKEN_RE.test(s)) return ANY;
|
|
15772
|
+
if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
|
|
15773
|
+
return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
|
|
15496
15774
|
});
|
|
15497
15775
|
if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
|
|
15498
15776
|
}
|
|
@@ -15500,6 +15778,38 @@ function normalizeClauses(raw) {
|
|
|
15500
15778
|
}
|
|
15501
15779
|
return out;
|
|
15502
15780
|
}
|
|
15781
|
+
function recordTypesOf(actions) {
|
|
15782
|
+
if (actions.includes("*")) return "all";
|
|
15783
|
+
const types = /* @__PURE__ */ new Set();
|
|
15784
|
+
let sawRecordsAction = false;
|
|
15785
|
+
let sawUntyped = false;
|
|
15786
|
+
for (const action of actions) {
|
|
15787
|
+
const parts = action.split(":");
|
|
15788
|
+
if (parts[0] !== "records") continue;
|
|
15789
|
+
if (!(parts[1] ?? "").includes("r")) continue;
|
|
15790
|
+
if (parts.length >= 3) {
|
|
15791
|
+
const type = parts.slice(2).join(":");
|
|
15792
|
+
if (type === "") continue;
|
|
15793
|
+
if (type === "*") continue;
|
|
15794
|
+
if (type.endsWith(":*")) {
|
|
15795
|
+
const stripped = type.slice(0, -2);
|
|
15796
|
+
if (stripped === "" || stripped.includes(":")) continue;
|
|
15797
|
+
sawRecordsAction = true;
|
|
15798
|
+
types.add(stripped);
|
|
15799
|
+
continue;
|
|
15800
|
+
}
|
|
15801
|
+
if (type.includes(":")) continue;
|
|
15802
|
+
sawRecordsAction = true;
|
|
15803
|
+
types.add(type);
|
|
15804
|
+
} else {
|
|
15805
|
+
sawRecordsAction = true;
|
|
15806
|
+
sawUntyped = true;
|
|
15807
|
+
}
|
|
15808
|
+
}
|
|
15809
|
+
if (!sawRecordsAction) return null;
|
|
15810
|
+
if (sawUntyped) return "all";
|
|
15811
|
+
return [...types].sort();
|
|
15812
|
+
}
|
|
15503
15813
|
function buildArchetypes(clauses) {
|
|
15504
15814
|
const rows = [
|
|
15505
15815
|
{ label: "a record you own alone (private)", owners: { user: SELF } },
|
|
@@ -15510,8 +15820,13 @@ function buildArchetypes(clauses) {
|
|
|
15510
15820
|
if (c.unrecognizedDims.length > 0) continue;
|
|
15511
15821
|
for (const [dim, values] of Object.entries(c.dataScope)) {
|
|
15512
15822
|
if (dim === "user") continue;
|
|
15823
|
+
if (dim === DIM_WILDCARD) continue;
|
|
15513
15824
|
const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
|
|
15514
|
-
|
|
15825
|
+
set.add(SELF);
|
|
15826
|
+
for (const v of values) {
|
|
15827
|
+
if (v === null || v === ANY || v === UNDER_SELF) continue;
|
|
15828
|
+
set.add(v);
|
|
15829
|
+
}
|
|
15515
15830
|
scopeDims.set(dim, set);
|
|
15516
15831
|
}
|
|
15517
15832
|
}
|
|
@@ -15530,19 +15845,42 @@ function buildArchetypes(clauses) {
|
|
|
15530
15845
|
rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
|
|
15531
15846
|
return rows;
|
|
15532
15847
|
}
|
|
15848
|
+
function listAdmits(allowed, recordValue) {
|
|
15849
|
+
return allowed.some((a) => {
|
|
15850
|
+
if (a === null) return recordValue === void 0;
|
|
15851
|
+
if (a === SELF) return recordValue === SELF;
|
|
15852
|
+
if (a === ANY) return recordValue !== void 0;
|
|
15853
|
+
if (a === UNDER_SELF) return false;
|
|
15854
|
+
return recordValue === a;
|
|
15855
|
+
});
|
|
15856
|
+
}
|
|
15533
15857
|
function clauseMatches(clause, arch) {
|
|
15534
15858
|
if (clause.unrecognizedDims.length > 0) return false;
|
|
15859
|
+
const wildcard = clause.dataScope[DIM_WILDCARD];
|
|
15535
15860
|
for (const [dim, allowed] of Object.entries(clause.dataScope)) {
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15861
|
+
if (dim === DIM_WILDCARD) continue;
|
|
15862
|
+
if (!listAdmits(allowed, arch.owners[dim])) return false;
|
|
15863
|
+
}
|
|
15864
|
+
if (wildcard) {
|
|
15865
|
+
let carriesAny = false;
|
|
15866
|
+
for (const [dim, value] of Object.entries(arch.owners)) {
|
|
15867
|
+
carriesAny = true;
|
|
15868
|
+
if (dim in clause.dataScope) continue;
|
|
15869
|
+
if (!listAdmits(wildcard, value)) return false;
|
|
15870
|
+
}
|
|
15871
|
+
if (!carriesAny && !listAdmits(wildcard, void 0)) return false;
|
|
15543
15872
|
}
|
|
15544
15873
|
return true;
|
|
15545
15874
|
}
|
|
15875
|
+
function undecidableDims(clauses) {
|
|
15876
|
+
const dims = /* @__PURE__ */ new Set();
|
|
15877
|
+
for (const c of clauses) {
|
|
15878
|
+
for (const [dim, values] of Object.entries(c.dataScope)) {
|
|
15879
|
+
if (values.includes(UNDER_SELF)) dims.add(dim);
|
|
15880
|
+
}
|
|
15881
|
+
}
|
|
15882
|
+
return [...dims];
|
|
15883
|
+
}
|
|
15546
15884
|
function explainClauses(clauses) {
|
|
15547
15885
|
const archetypes = buildArchetypes(clauses);
|
|
15548
15886
|
return archetypes.map((arch) => {
|
|
@@ -15554,15 +15892,34 @@ function explainClauses(clauses) {
|
|
|
15554
15892
|
for (const a of c.actions) actions.add(a);
|
|
15555
15893
|
}
|
|
15556
15894
|
});
|
|
15557
|
-
|
|
15895
|
+
const recordTypes = recordTypesOf([...actions]);
|
|
15896
|
+
return {
|
|
15897
|
+
archetype: arch.label,
|
|
15898
|
+
clauses: matched,
|
|
15899
|
+
actions: [...actions],
|
|
15900
|
+
...recordTypes !== null ? { recordTypes } : {}
|
|
15901
|
+
};
|
|
15558
15902
|
});
|
|
15559
15903
|
}
|
|
15560
15904
|
function renderDataScope(ds) {
|
|
15561
15905
|
const entries = Object.entries(ds);
|
|
15562
15906
|
if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
|
|
15563
|
-
return entries.map(
|
|
15564
|
-
|
|
15565
|
-
|
|
15907
|
+
return entries.map(([dim, values]) => {
|
|
15908
|
+
const shown = values.map((v) => {
|
|
15909
|
+
if (v === SELF) return "self";
|
|
15910
|
+
if (v === ANY) return "any";
|
|
15911
|
+
if (v === UNDER_SELF) return "under-self";
|
|
15912
|
+
if (v === null) return "null";
|
|
15913
|
+
return v;
|
|
15914
|
+
});
|
|
15915
|
+
const label = dim === DIM_WILDCARD ? "* (every other dimension)" : dim;
|
|
15916
|
+
return `${label}:[${shown.join(", ")}]`;
|
|
15917
|
+
}).join(" AND ");
|
|
15918
|
+
}
|
|
15919
|
+
function renderRecordTypes(recordTypes) {
|
|
15920
|
+
if (recordTypes === void 0) return "\u2014";
|
|
15921
|
+
if (recordTypes === "all") return "all types";
|
|
15922
|
+
return recordTypes.join(", ");
|
|
15566
15923
|
}
|
|
15567
15924
|
var SELF_PRINCIPAL_ALIAS2 = "me";
|
|
15568
15925
|
async function runAccessExplain(opts, deps = {}) {
|
|
@@ -15590,6 +15947,7 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
15590
15947
|
}
|
|
15591
15948
|
const clauses = normalizeClauses(rawClauses);
|
|
15592
15949
|
const rows = explainClauses(clauses);
|
|
15950
|
+
const undecidable = undecidableDims(clauses);
|
|
15593
15951
|
if (opts.json) {
|
|
15594
15952
|
process.stdout.write(
|
|
15595
15953
|
`${formatJson({
|
|
@@ -15606,7 +15964,10 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
15606
15964
|
...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
|
|
15607
15965
|
})),
|
|
15608
15966
|
matrix: rows,
|
|
15609
|
-
|
|
15967
|
+
// Present only when a clause uses `${{ under.self.* }}`: those rows are UNDECIDED
|
|
15968
|
+
// offline, not denied, and a consumer reading a blank cell as a denial would be wrong.
|
|
15969
|
+
...undecidable.length > 0 ? { undecidableDims: undecidable } : {},
|
|
15970
|
+
note: "This preview models READ reachability, not write authorization: a `c`/`u`/`d` verb shown granted for a row may not actually be able to create/update a record with that exact ownership shape, because the platform requires a clause to explicitly name a dimension to place a value into it, while an unmentioned dimension never narrows reads. matrix[].recordTypes is the `records:` reach of that row's grant, separate from the ownership archetype: \"all\" reaches every type, an array names the type(s), absent means no `records:` action grants this row. Search/RAG visibility is a separate, still-unmodeled narrowing bounded per row by the same clause's typed read grants. Client-side preview; the platform decides at request time."
|
|
15610
15971
|
})}
|
|
15611
15972
|
`
|
|
15612
15973
|
);
|
|
@@ -15634,8 +15995,19 @@ async function runAccessExplain(opts, deps = {}) {
|
|
|
15634
15995
|
"\n" + formatTable(rows, [
|
|
15635
15996
|
{ header: "record owned by\u2026", get: (r) => r.archetype },
|
|
15636
15997
|
{ header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
|
|
15637
|
-
{ header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
|
|
15638
|
-
|
|
15998
|
+
{ header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" },
|
|
15999
|
+
// The ownership "granted?" cell says nothing about WHICH record types — a clause can be
|
|
16000
|
+
// ownership-unscoped and still type-restricted (a curated shared-KB clause with no owner
|
|
16001
|
+
// dimension, readable only for a few named types, never others). Surface the actual
|
|
16002
|
+
// `records:` reach per row instead of leaving it to a footnote.
|
|
16003
|
+
{ header: "record types", get: (r) => renderRecordTypes(r.recordTypes) }
|
|
16004
|
+
]) + (undecidable.length > 0 ? `
|
|
16005
|
+
|
|
16006
|
+
\u26A0 UNDECIDED offline: ${undecidable.join(", ")}
|
|
16007
|
+
A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
|
|
16008
|
+
so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
|
|
16009
|
+
NOT as a denial \u2014 the matrix under-reports this clause.
|
|
16010
|
+
` : "") + '\n\n \u26A0 READ ONLY: "granted?"/"actions" model whether a row can be READ, not whether a c/u/d\n verb shown there can actually WRITE that row\'s exact shape \u2014 the platform requires a clause\n to name a dimension to place a value into it, while reads are never narrowed by a dimension a\n clause omits. A create/update verb next to a row is advisory: it names which records exist to\n act on, not that a specific write will succeed.\n\n\n NOTE: this matrix models the OWNERSHIP dimensions only \u2014 "granted?" says a row\'s ownership\n shape matches a clause, and "record types" (above) says which `records:` types that grant\n actually reaches; "all" reaches every type, "\u2014" means no `records:` action grants this row at\n all. Search/RAG visibility is a separate, still-unmodeled narrowing: it is bounded per row by\n the SAME clause\'s typed read grants \u2014 a row shown with `search:r` is searchable only for the\n 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'
|
|
15639
16011
|
);
|
|
15640
16012
|
return 0;
|
|
15641
16013
|
} catch (err) {
|
|
@@ -18374,9 +18746,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
|
|
|
18374
18746
|
|
|
18375
18747
|
// src/build-info.ts
|
|
18376
18748
|
var BUILD_INFO = {
|
|
18377
|
-
cli: true ? "0.
|
|
18378
|
-
sdk: true ? "0.
|
|
18379
|
-
blueprints: true ? "0.
|
|
18749
|
+
cli: true ? "0.13.0" : "dev",
|
|
18750
|
+
sdk: true ? "0.38.0" : "dev",
|
|
18751
|
+
blueprints: true ? "0.10.0" : "dev"
|
|
18380
18752
|
};
|
|
18381
18753
|
function formatBuildInfo() {
|
|
18382
18754
|
return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
|