@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/dist/cli.mjs CHANGED
@@ -4048,7 +4048,7 @@ var AuthClient = class {
4048
4048
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/access-log");
4049
4049
  }
4050
4050
  /**
4051
- * Lists all of your scoped API keys (`ssk_*`) across both your live and test environments. Revoked keys are excluded. Requires the `keys:r` scope.
4051
+ * 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.
4052
4052
  *
4053
4053
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
4054
4054
  *
@@ -4431,7 +4431,7 @@ var AuthClient = class {
4431
4431
  );
4432
4432
  }
4433
4433
  /**
4434
- * 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.
4434
+ * 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.
4435
4435
  *
4436
4436
  * @param {Vectros.CreateAccessProfileRequest} request
4437
4437
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4849,7 +4849,7 @@ var AuthClient = class {
4849
4849
  );
4850
4850
  }
4851
4851
  /**
4852
- * 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; a root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
4852
+ * 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.
4853
4853
  *
4854
4854
  * @param {Vectros.UpdateAccessProfileRequest} request
4855
4855
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4922,7 +4922,7 @@ var AuthClient = class {
4922
4922
  );
4923
4923
  }
4924
4924
  /**
4925
- * 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.
4925
+ * 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.
4926
4926
  *
4927
4927
  * @param {Vectros.DeleteAccessProfileRequest} request
4928
4928
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5776,7 +5776,7 @@ var AuthClient = class {
5776
5776
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
5777
5777
  }
5778
5778
  /**
5779
- * 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 `admin:users` scope.
5779
+ * 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.
5780
5780
  *
5781
5781
  * @param {Vectros.CreateInviteRequest} request
5782
5782
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5847,7 +5847,7 @@ var AuthClient = class {
5847
5847
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
5848
5848
  }
5849
5849
  /**
5850
- * 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. Requires the `admin:users` scope.
5850
+ * 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.
5851
5851
  *
5852
5852
  * @param {Vectros.CreateInviteRequest} request
5853
5853
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6181,7 +6181,7 @@ var DocumentsClient = class {
6181
6181
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6182
6182
  * scope: "group:eng-team",
6183
6183
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6184
- * startFrom: "doc_prev123"
6184
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6185
6185
  * })
6186
6186
  */
6187
6187
  listDocuments(request = {}, requestOptions) {
@@ -6491,6 +6491,7 @@ var DocumentsClient = class {
6491
6491
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
6492
6492
  *
6493
6493
  * @throws {@link Vectros.BadRequestError}
6494
+ * @throws {@link Vectros.ForbiddenError}
6494
6495
  * @throws {@link Vectros.NotFoundError}
6495
6496
  * @throws {@link Vectros.ConflictError}
6496
6497
  * @throws {@link Vectros.TooManyRequestsError}
@@ -6538,6 +6539,8 @@ var DocumentsClient = class {
6538
6539
  switch (_response.error.statusCode) {
6539
6540
  case 400:
6540
6541
  throw new BadRequestError(_response.error.body, _response.rawResponse);
6542
+ case 403:
6543
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6541
6544
  case 404:
6542
6545
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6543
6546
  case 409:
@@ -6568,7 +6571,7 @@ var DocumentsClient = class {
6568
6571
  * field: "po_number",
6569
6572
  * value: "PO-1001",
6570
6573
  * prefix: "PO-2024",
6571
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
6574
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
6572
6575
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6573
6576
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
6574
6577
  * })
@@ -6944,7 +6947,7 @@ var FoldersClient = class {
6944
6947
  * parentFolderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6945
6948
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6946
6949
  * scope: "group:eng-team",
6947
- * startFrom: "fld_prev123"
6950
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6948
6951
  * })
6949
6952
  */
6950
6953
  listFolders(request = {}, requestOptions) {
@@ -7121,6 +7124,7 @@ var FoldersClient = class {
7121
7124
  * @param {Vectros.UpdateFolderRequest} request
7122
7125
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7123
7126
  *
7127
+ * @throws {@link Vectros.ForbiddenError}
7124
7128
  * @throws {@link Vectros.NotFoundError}
7125
7129
  * @throws {@link Vectros.TooManyRequestsError}
7126
7130
  *
@@ -7165,6 +7169,8 @@ var FoldersClient = class {
7165
7169
  }
7166
7170
  if (_response.error.reason === "status-code") {
7167
7171
  switch (_response.error.statusCode) {
7172
+ case 403:
7173
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7168
7174
  case 404:
7169
7175
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7170
7176
  case 429:
@@ -7247,6 +7253,7 @@ var FoldersClient = class {
7247
7253
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7248
7254
  *
7249
7255
  * @throws {@link Vectros.BadRequestError}
7256
+ * @throws {@link Vectros.ForbiddenError}
7250
7257
  * @throws {@link Vectros.NotFoundError}
7251
7258
  * @throws {@link Vectros.ConflictError}
7252
7259
  * @throws {@link Vectros.TooManyRequestsError}
@@ -7294,6 +7301,8 @@ var FoldersClient = class {
7294
7301
  switch (_response.error.statusCode) {
7295
7302
  case 400:
7296
7303
  throw new BadRequestError(_response.error.body, _response.rawResponse);
7304
+ case 403:
7305
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7297
7306
  case 404:
7298
7307
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7299
7308
  case 409:
@@ -7381,7 +7390,8 @@ var IdentityClient = class {
7381
7390
  *
7382
7391
  * @example
7383
7392
  * await client.identity.listEntities({
7384
- * namespace: "team"
7393
+ * namespace: "team",
7394
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7385
7395
  * })
7386
7396
  */
7387
7397
  listEntities(request, requestOptions) {
@@ -8145,7 +8155,7 @@ var IdentityClient = class {
8145
8155
  * @example
8146
8156
  * await client.identity.listUsers({
8147
8157
  * externalId: "usr_12345",
8148
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
8158
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
8149
8159
  * type: "person_v1",
8150
8160
  * field: "team",
8151
8161
  * value: "engineering",
@@ -8326,11 +8336,12 @@ var IdentityClient = class {
8326
8336
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8327
8337
  }
8328
8338
  /**
8329
- * 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.
8339
+ * 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.
8330
8340
  *
8331
8341
  * @param {Vectros.UpdateUserRequest} request
8332
8342
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8333
8343
  *
8344
+ * @throws {@link Vectros.BadRequestError}
8334
8345
  * @throws {@link Vectros.NotFoundError}
8335
8346
  * @throws {@link Vectros.TooManyRequestsError}
8336
8347
  *
@@ -8375,6 +8386,8 @@ var IdentityClient = class {
8375
8386
  }
8376
8387
  if (_response.error.reason === "status-code") {
8377
8388
  switch (_response.error.statusCode) {
8389
+ case 400:
8390
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8378
8391
  case 404:
8379
8392
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8380
8393
  case 429:
@@ -9022,7 +9035,7 @@ var RecordsClient = class {
9022
9035
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
9023
9036
  * userId: "550e8400-e29b-41d4-a716-446655440000",
9024
9037
  * scope: "group:eng-team",
9025
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9038
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9026
9039
  * })
9027
9040
  */
9028
9041
  listRecords(request = {}, requestOptions) {
@@ -9210,6 +9223,7 @@ var RecordsClient = class {
9210
9223
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9211
9224
  *
9212
9225
  * @throws {@link Vectros.BadRequestError}
9226
+ * @throws {@link Vectros.ForbiddenError}
9213
9227
  * @throws {@link Vectros.NotFoundError}
9214
9228
  * @throws {@link Vectros.ConflictError}
9215
9229
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9258,6 +9272,8 @@ var RecordsClient = class {
9258
9272
  switch (_response.error.statusCode) {
9259
9273
  case 400:
9260
9274
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9275
+ case 403:
9276
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9261
9277
  case 404:
9262
9278
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9263
9279
  case 409:
@@ -9339,6 +9355,7 @@ var RecordsClient = class {
9339
9355
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9340
9356
  *
9341
9357
  * @throws {@link Vectros.BadRequestError}
9358
+ * @throws {@link Vectros.ForbiddenError}
9342
9359
  * @throws {@link Vectros.NotFoundError}
9343
9360
  * @throws {@link Vectros.ConflictError}
9344
9361
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9389,6 +9406,8 @@ var RecordsClient = class {
9389
9406
  switch (_response.error.statusCode) {
9390
9407
  case 400:
9391
9408
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9409
+ case 403:
9410
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9392
9411
  case 404:
9393
9412
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9394
9413
  case 409:
@@ -9406,7 +9425,15 @@ var RecordsClient = class {
9406
9425
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/records/{id}");
9407
9426
  }
9408
9427
  /**
9409
- * 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. Results are paginated: set `limit` for the page size and pass the returned `nextCursor` back as `startFrom` for the next page. Requires the `records:r:<type>` scope.
9428
+ * 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.
9429
+ *
9430
+ * 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`.
9431
+ *
9432
+ * 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.
9433
+ *
9434
+ * 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.
9435
+ *
9436
+ * 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.
9410
9437
  *
9411
9438
  * @param {Vectros.LookupRecordsRequest} request
9412
9439
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9418,22 +9445,40 @@ var RecordsClient = class {
9418
9445
  * type: "intake_form",
9419
9446
  * field: "email",
9420
9447
  * value: "jane@example.com",
9448
+ * values: ["open"],
9421
9449
  * prefix: "jane",
9422
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9450
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9423
9451
  * })
9424
9452
  */
9425
9453
  lookupRecords(request, requestOptions) {
9426
9454
  return HttpResponsePromise.fromPromise(this.__lookupRecords(request, requestOptions));
9427
9455
  }
9428
9456
  async __lookupRecords(request, requestOptions) {
9429
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, includePayload, order } = request;
9457
+ const {
9458
+ type: type_,
9459
+ field,
9460
+ value,
9461
+ values,
9462
+ from: from_,
9463
+ to,
9464
+ prefix,
9465
+ sortFrom,
9466
+ sortTo,
9467
+ startFrom,
9468
+ limit,
9469
+ includePayload,
9470
+ order
9471
+ } = request;
9430
9472
  const _queryParams = {
9431
9473
  type: type_,
9432
9474
  field,
9433
9475
  value,
9476
+ values,
9434
9477
  from: from_,
9435
9478
  to,
9436
9479
  prefix,
9480
+ sortFrom,
9481
+ sortTo,
9437
9482
  startFrom,
9438
9483
  limit,
9439
9484
  includePayload,
@@ -9477,7 +9522,7 @@ var RecordsClient = class {
9477
9522
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records/lookup");
9478
9523
  }
9479
9524
  /**
9480
- * 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.
9525
+ * 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.
9481
9526
  *
9482
9527
  * @param {Vectros.RecordLookupRequest} request
9483
9528
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9672,7 +9717,7 @@ var SchemasClient = class {
9672
9717
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
9673
9718
  * surface: "document",
9674
9719
  * recordType: "intake_form",
9675
- * startFrom: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
9720
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9676
9721
  * })
9677
9722
  */
9678
9723
  listSchemas(request = {}, requestOptions) {
@@ -9876,6 +9921,7 @@ var SchemasClient = class {
9876
9921
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
9877
9922
  *
9878
9923
  * @throws {@link Vectros.BadRequestError}
9924
+ * @throws {@link Vectros.ForbiddenError}
9879
9925
  * @throws {@link Vectros.NotFoundError}
9880
9926
  * @throws {@link Vectros.TooManyRequestsError}
9881
9927
  *
@@ -9924,6 +9970,8 @@ var SchemasClient = class {
9924
9970
  switch (_response.error.statusCode) {
9925
9971
  case 400:
9926
9972
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9973
+ case 403:
9974
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9927
9975
  case 404:
9928
9976
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9929
9977
  case 429:
@@ -10190,7 +10238,7 @@ var logging;
10190
10238
  logging2.ConsoleLogger = ConsoleLogger;
10191
10239
  })(logging || (logging = {}));
10192
10240
 
10193
- // ../blueprints/dist/index.mjs
10241
+ // node_modules/@vectros-ai/blueprints/dist/index.mjs
10194
10242
  import { z } from "zod";
10195
10243
  import { z as z2 } from "zod";
10196
10244
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -10294,19 +10342,63 @@ var BlueprintFieldDefSchema = z.object({
10294
10342
  var BlueprintLookupFieldSchema = z.union([
10295
10343
  z.string().min(1),
10296
10344
  z.object({
10297
- fieldName: z.string().min(1),
10345
+ // A lookup field declares either ONE field (fieldName) or 2-3 fields
10346
+ // matched together (fieldNames) — never both, never neither. `.strict()`
10347
+ // below only rejects unknown KEYS; it does not enforce this combination,
10348
+ // so the `.superRefine()` after it carries the actual rule.
10349
+ fieldName: z.string().min(1).optional(),
10350
+ // Composite (conjunctive) lookup: match on all of these fields at once —
10351
+ // "every record where `status` is `open` AND `area` is `billing`", exact,
10352
+ // in a stable order. Order is significant and locked at create: a query
10353
+ // may match a leading run of the list (the first field alone, the first
10354
+ // two together, …) but never a later field alone — declare a separate
10355
+ // lookup for that. Record-only: the schema's `allowedSurfaces` must be
10356
+ // exactly `['record']` (checked at the schema level below) — the
10357
+ // platform's composite index has no document/user/entity reader yet.
10358
+ fieldNames: z.array(z.string().min(1)).min(2).max(3).optional(),
10359
+ // Not available on a composite (`fieldNames`) lookup — checked below.
10298
10360
  unique: z.boolean().optional(),
10299
10361
  // Opt this field into ordered range + prefix lookups (from/to/prefix) on
10300
10362
  // top of exact match. Billed at the range-index rate; not valid on a
10301
- // sensitive field (a blind index is not orderable). Locked at create.
10363
+ // sensitive field (a blind index is not orderable), and — like `unique`
10364
+ // — not available on a composite lookup: it is an exact-match index over
10365
+ // its fields; declare the range lookup separately. Locked at create.
10302
10366
  rangeEnabled: z.boolean().optional(),
10303
10367
  // Sort key for the exact-match index: 'createdAt' (default), 'lastUpdated',
10304
- // or a declared field on this schema. Locked at create.
10368
+ // or a declared field on this schema. Locked at create. Valid on a
10369
+ // composite lookup too — it orders WITHIN a group when a query supplies
10370
+ // fewer values than the lookup declares, not across the whole result.
10305
10371
  sortBy: z.string().min(1).optional(),
10306
10372
  // Opt a field past the fixed fast-index budget into a higher-cost
10307
10373
  // secondary index. No effect on a field that fits within the budget.
10308
10374
  allowOverflow: z.boolean().optional()
10309
- }).strict()
10375
+ }).strict().superRefine((lf, ctx) => {
10376
+ const hasFieldName = lf.fieldName !== void 0;
10377
+ const hasFieldNames = lf.fieldNames !== void 0;
10378
+ if (hasFieldName === hasFieldNames) {
10379
+ ctx.addIssue({
10380
+ code: z.ZodIssueCode.custom,
10381
+ message: "a lookup field declares either 'fieldName' (one field) or 'fieldNames' (2-3 fields matched together), never both and never neither"
10382
+ });
10383
+ return;
10384
+ }
10385
+ if (hasFieldNames) {
10386
+ if (lf.unique !== void 0) {
10387
+ ctx.addIssue({
10388
+ code: z.ZodIssueCode.custom,
10389
+ path: ["unique"],
10390
+ message: "'unique' is not available on a composite ('fieldNames') lookup"
10391
+ });
10392
+ }
10393
+ if (lf.rangeEnabled !== void 0) {
10394
+ ctx.addIssue({
10395
+ code: z.ZodIssueCode.custom,
10396
+ path: ["rangeEnabled"],
10397
+ message: "'rangeEnabled' is not available on a composite ('fieldNames') lookup"
10398
+ });
10399
+ }
10400
+ }
10401
+ })
10310
10402
  ]);
10311
10403
  var BlueprintCapabilitiesSchema = z.object({
10312
10404
  auditHistory: z.boolean().optional()
@@ -10344,7 +10436,21 @@ var BlueprintSchemaSchema = z.object({
10344
10436
  // hop); immutable once set. Mirrors the `basedOn` field on the platform's
10345
10437
  // schema-create request.
10346
10438
  basedOn: z.string().min(1).optional()
10347
- }).strict();
10439
+ }).strict().superRefine((schema, ctx) => {
10440
+ const hasComposite = (schema.lookupFields ?? []).some(
10441
+ (lf) => typeof lf === "object" && lf.fieldNames !== void 0
10442
+ );
10443
+ if (!hasComposite) return;
10444
+ const surfaces = schema.allowedSurfaces;
10445
+ const isRecordOnly = surfaces === void 0 || surfaces.length === 1 && surfaces[0] === "record";
10446
+ if (!isRecordOnly) {
10447
+ ctx.addIssue({
10448
+ code: z.ZodIssueCode.custom,
10449
+ path: ["allowedSurfaces"],
10450
+ 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`
10451
+ });
10452
+ }
10453
+ });
10348
10454
  var BlueprintAccessProfileSchema = z.object({
10349
10455
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10350
10456
  // what enforces the data-plane-only security boundary.
@@ -12283,9 +12389,10 @@ var secondBrain = {
12283
12389
  // Lookup shapes are MIGRATION-LOCKED once the schema is live (a field cannot
12284
12390
  // flip equality↔range later), so each is chosen on purpose:
12285
12391
  // • `source` — EQUALITY, sorted by `lastUpdated`: enumerate notes from one
12286
- // source, most-recently-touched first. `lastUpdated` is always present, so
12287
- // the sort never drops a note (sorting an equality lookup by an OPTIONAL
12288
- // field would silently exclude rows that lack it).
12392
+ // source, most-recently-touched first. A platform timestamp is the natural
12393
+ // choice for "most recently touched" and is always present, so every note
12394
+ // has a position. (Sorting by an OPTIONAL payload field is also fine — notes
12395
+ // lacking it are ordered ahead of those that have it, never dropped.)
12289
12396
  // • `status` — EQUALITY: "show archived notes".
12290
12397
  // • `capturedAt` — RANGE: ordered `from`/`to`/`prefix` ("captured in 2026-06").
12291
12398
  // `externalId` has a built-in first-class finder — look it up directly, never
@@ -13407,7 +13514,16 @@ function toSdkFields(blueprint, schemaIndex) {
13407
13514
  function toLookupDefs(lookupFields) {
13408
13515
  return lookupFields?.map((lf) => {
13409
13516
  if (typeof lf === "string") return { fieldName: lf };
13410
- const def = { fieldName: lf.fieldName };
13517
+ const hasFieldName = lf.fieldName !== void 0;
13518
+ const hasFieldNames = lf.fieldNames !== void 0;
13519
+ if (hasFieldName === hasFieldNames) {
13520
+ throw new Error(
13521
+ `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.`
13522
+ );
13523
+ }
13524
+ const def = {};
13525
+ if (lf.fieldName !== void 0) def.fieldName = lf.fieldName;
13526
+ if (lf.fieldNames !== void 0) def.fieldNames = lf.fieldNames;
13411
13527
  if (lf.unique !== void 0) def.unique = lf.unique;
13412
13528
  if (lf.rangeEnabled !== void 0) def.rangeEnabled = lf.rangeEnabled;
13413
13529
  if (lf.sortBy !== void 0) def.sortBy = lf.sortBy;
@@ -13670,7 +13786,7 @@ function buildPlan(blueprint, opts = {}) {
13670
13786
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13671
13787
  });
13672
13788
  for (const s of blueprint.schemas) {
13673
- const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldName) ?? [];
13789
+ const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13674
13790
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
13675
13791
  const surfaceNote = s.allowedSurfaces ? ` surfaces: [${s.allowedSurfaces.join(", ")}]` : "";
13676
13792
  const sensitive = s.fields.filter((f) => f.sensitive).map((f) => f.fieldId);
@@ -14262,10 +14378,121 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
14262
14378
  // src/cli-blueprint.ts
14263
14379
  import { writeFile as writeFile2, access } from "fs/promises";
14264
14380
 
14381
+ // src/scope-grammar.ts
14382
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
14383
+ "user",
14384
+ "record",
14385
+ "document",
14386
+ "entity",
14387
+ "self",
14388
+ "tenant",
14389
+ "context",
14390
+ "scope",
14391
+ "versions",
14392
+ "lookup"
14393
+ ]);
14394
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
14395
+ var SCOPE_NAMESPACE_MIN_LENGTH = 2;
14396
+ var SCOPE_NAMESPACE_MAX_LENGTH = 32;
14397
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
14398
+ var SCOPE_VALUE_MIN_LENGTH = 1;
14399
+ var SCOPE_VALUE_MAX_LENGTH = 128;
14400
+ function valueGrammarError(value) {
14401
+ if (!SCOPE_VALUE_RE.test(value)) {
14402
+ 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.`;
14403
+ }
14404
+ return null;
14405
+ }
14406
+ function namespaceGrammarError(ns) {
14407
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
14408
+ return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
14409
+ }
14410
+ if (!SCOPE_NAMESPACE_RE.test(ns)) {
14411
+ 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 '-'`;
14412
+ }
14413
+ return null;
14414
+ }
14415
+ function assertScopeEntries(entries, flag) {
14416
+ if (!entries?.length) return;
14417
+ for (const raw of entries) {
14418
+ const sep = raw.indexOf(":");
14419
+ if (sep <= 0 || sep === raw.length - 1) {
14420
+ throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14421
+ }
14422
+ const nsError = namespaceGrammarError(raw.slice(0, sep));
14423
+ if (nsError) throw new Error(`${flag} namespace ${nsError}`);
14424
+ const valError = valueGrammarError(raw.slice(sep + 1));
14425
+ if (valError) throw new Error(`${flag} value ${valError}`);
14426
+ }
14427
+ }
14428
+ function assertIdentityOverrideValues(overrides, flag) {
14429
+ if (!overrides) return;
14430
+ for (const [key, value] of Object.entries(overrides)) {
14431
+ if (!key.startsWith("scope:")) continue;
14432
+ const error = valueGrammarError(typeof value === "string" ? value : String(value));
14433
+ if (error) throw new Error(`${flag} '${key}' value ${error}`);
14434
+ }
14435
+ }
14436
+ function placeholderScopeNamespace(value) {
14437
+ const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
14438
+ return m ? m[1] : null;
14439
+ }
14440
+ var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
14441
+ var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
14442
+
14265
14443
  // src/lint.ts
14266
14444
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
14267
- function lookupFieldName(lf) {
14268
- return typeof lf === "string" ? lf : lf.fieldName;
14445
+ var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
14446
+ function lookupFieldNames(lf) {
14447
+ if (typeof lf === "string") return [lf];
14448
+ if (lf.fieldNames !== void 0) return lf.fieldNames;
14449
+ return lf.fieldName !== void 0 ? [lf.fieldName] : [];
14450
+ }
14451
+ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
14452
+ const findings = [];
14453
+ const id = `[${lf.fieldNames.join(", ")}]`;
14454
+ const seenLegs = /* @__PURE__ */ new Set();
14455
+ for (const leg of lf.fieldNames) {
14456
+ if (seenLegs.has(leg)) {
14457
+ findings.push({
14458
+ severity: "error",
14459
+ rule: "lookup-composite-repeated-leg",
14460
+ 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.`
14461
+ });
14462
+ }
14463
+ seenLegs.add(leg);
14464
+ const field = fieldsById.get(leg);
14465
+ if (!field) continue;
14466
+ if (field.fieldType === "array" || field.fieldType === "object") {
14467
+ findings.push({
14468
+ severity: "error",
14469
+ rule: "lookup-composite-leg-multi-valued",
14470
+ 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.`
14471
+ });
14472
+ }
14473
+ if (field.fieldType === "reference" && field.cardinality === "many") {
14474
+ findings.push({
14475
+ severity: "error",
14476
+ rule: "lookup-composite-leg-multi-valued",
14477
+ 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.`
14478
+ });
14479
+ }
14480
+ if (rangeEnabledFieldNames.has(leg)) {
14481
+ findings.push({
14482
+ severity: "error",
14483
+ rule: "lookup-composite-leg-range-enabled",
14484
+ 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.`
14485
+ });
14486
+ }
14487
+ if (lf.sortBy !== void 0 && leg === lf.sortBy) {
14488
+ findings.push({
14489
+ severity: "error",
14490
+ rule: "lookup-composite-leg-is-sortby",
14491
+ 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'.`
14492
+ });
14493
+ }
14494
+ }
14495
+ return findings;
14269
14496
  }
14270
14497
  function lintBlueprint(bp) {
14271
14498
  const findings = [];
@@ -14273,6 +14500,7 @@ function lintBlueprint(bp) {
14273
14500
  for (const schema of bp.schemas) {
14274
14501
  typeNames.add(schema.typeName);
14275
14502
  const fieldIds = /* @__PURE__ */ new Set();
14503
+ const fieldsById = /* @__PURE__ */ new Map();
14276
14504
  for (const f of schema.fields) {
14277
14505
  if (fieldIds.has(f.fieldId)) {
14278
14506
  findings.push({
@@ -14282,6 +14510,7 @@ function lintBlueprint(bp) {
14282
14510
  });
14283
14511
  }
14284
14512
  fieldIds.add(f.fieldId);
14513
+ fieldsById.set(f.fieldId, f);
14285
14514
  if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
14286
14515
  findings.push({
14287
14516
  severity: "warning",
@@ -14290,14 +14519,35 @@ function lintBlueprint(bp) {
14290
14519
  });
14291
14520
  }
14292
14521
  }
14522
+ const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
14293
14523
  for (const lf of schema.lookupFields ?? []) {
14294
- const name = lookupFieldName(lf);
14295
- if (!fieldIds.has(name)) {
14296
- findings.push({
14297
- severity: "error",
14298
- rule: "lookup-field-unknown",
14299
- message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14300
- });
14524
+ if (typeof lf !== "string" && lf.fieldName !== void 0 && lf.rangeEnabled) {
14525
+ rangeEnabledFieldNames.add(lf.fieldName);
14526
+ }
14527
+ }
14528
+ for (const lf of schema.lookupFields ?? []) {
14529
+ for (const name of lookupFieldNames(lf)) {
14530
+ if (!fieldIds.has(name)) {
14531
+ findings.push({
14532
+ severity: "error",
14533
+ rule: "lookup-field-unknown",
14534
+ message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14535
+ });
14536
+ }
14537
+ }
14538
+ if (typeof lf !== "string" && lf.fieldNames !== void 0) {
14539
+ findings.push(
14540
+ ...compositeLegFindings(schema, { fieldNames: lf.fieldNames, sortBy: lf.sortBy }, fieldsById, rangeEnabledFieldNames)
14541
+ );
14542
+ }
14543
+ if (typeof lf !== "string" && lf.sortBy !== void 0) {
14544
+ if (!BUILT_IN_SORT_BY.has(lf.sortBy) && !fieldIds.has(lf.sortBy)) {
14545
+ findings.push({
14546
+ severity: "warning",
14547
+ rule: "lookup-sortby-unknown-field",
14548
+ 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.`
14549
+ });
14550
+ }
14301
14551
  }
14302
14552
  }
14303
14553
  }
@@ -14313,6 +14563,10 @@ function lintBlueprint(bp) {
14313
14563
  const ds = bp.accessProfile.dataScope;
14314
14564
  if (ds) {
14315
14565
  for (const [key, values] of Object.entries(ds)) {
14566
+ for (const value of values) {
14567
+ const bad = placeholderFinding("accessProfile", key, value);
14568
+ if (bad) findings.push(bad);
14569
+ }
14316
14570
  if (values.length > 0 && !values.includes(null)) {
14317
14571
  findings.push({
14318
14572
  severity: "warning",
@@ -14336,6 +14590,10 @@ function lintBlueprint(bp) {
14336
14590
  const cds = clause.dataScope;
14337
14591
  if (!cds) return;
14338
14592
  for (const [key, values] of Object.entries(cds)) {
14593
+ for (const value of values) {
14594
+ const bad = placeholderFinding(`role '${roleId}' clause ${i}`, key, value);
14595
+ if (bad) findings.push(bad);
14596
+ }
14339
14597
  if (values.length > 0 && !values.includes(null)) {
14340
14598
  findings.push({
14341
14599
  severity: "warning",
@@ -14349,6 +14607,28 @@ function lintBlueprint(bp) {
14349
14607
  return findings;
14350
14608
  }
14351
14609
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
14610
+ var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
14611
+ var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
14612
+ var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
14613
+ function placeholderFinding(where, key, value) {
14614
+ if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
14615
+ if (BLUEPRINT_TOKEN_RE.test(value)) return null;
14616
+ if (RECOGNIZED_PLACEHOLDER_RES.some((re) => re.test(value))) {
14617
+ const ns = placeholderScopeNamespace(value);
14618
+ const nsError = ns === null ? null : namespaceGrammarError(ns);
14619
+ if (nsError === null) return null;
14620
+ return {
14621
+ severity: "error",
14622
+ rule: "datascope-unknown-placeholder",
14623
+ message: `${where}: dataScope.${key} value '${value}' names a namespace the platform rejects \u2014 ${nsError}. It will be rejected at apply.`
14624
+ };
14625
+ }
14626
+ return {
14627
+ severity: "error",
14628
+ rule: "datascope-unknown-placeholder",
14629
+ 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.`
14630
+ };
14631
+ }
14352
14632
  function lintPasses(findings) {
14353
14633
  return !findings.some((f) => f.severity === "error");
14354
14634
  }
@@ -14752,11 +15032,14 @@ function parseScopeOverrides(scopes) {
14752
15032
  throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14753
15033
  }
14754
15034
  const ns = raw.slice(0, sep);
15035
+ const value = raw.slice(sep + 1);
15036
+ const valueError = valueGrammarError(value);
15037
+ if (valueError) throw new Error(`--scope value ${valueError}`);
14755
15038
  const key = `scope:${ns}`;
14756
15039
  if (Object.prototype.hasOwnProperty.call(out, key)) {
14757
15040
  throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
14758
15041
  }
14759
- out[key] = raw.slice(sep + 1);
15042
+ out[key] = value;
14760
15043
  }
14761
15044
  if (Object.keys(out).length > 2) {
14762
15045
  throw new Error("--scope allows at most two distinct namespaces.");
@@ -15101,6 +15384,7 @@ async function createIdentity(client, opts, payload) {
15101
15384
  }
15102
15385
  });
15103
15386
  }
15387
+ assertScopeEntries(opts.scope, "--scope");
15104
15388
  return client.identity.createEntity({
15105
15389
  namespace: opts.type,
15106
15390
  body: {
@@ -15306,6 +15590,7 @@ async function runAccessGrant(opts, deps = {}) {
15306
15590
  ...parseScopeOverrides(opts.scope) ?? {},
15307
15591
  ...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
15308
15592
  };
15593
+ assertIdentityOverrideValues(identityOverrides, "--identity-overrides");
15309
15594
  } catch (err) {
15310
15595
  process.stderr.write(` \u2716 ${formatError(err)}
15311
15596
  `);
@@ -15458,30 +15743,21 @@ ${renderProfile(p)}
15458
15743
 
15459
15744
  // src/commands/access-explain.ts
15460
15745
  var SELF = /* @__PURE__ */ Symbol("self");
15746
+ var ANY = /* @__PURE__ */ Symbol("any");
15747
+ var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
15461
15748
  function canonicalDim(key) {
15462
15749
  if (key === "userId" || key === "partnerUserId") return "user";
15463
15750
  return key;
15464
15751
  }
15465
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
15466
- "user",
15467
- "record",
15468
- "document",
15469
- "entity",
15470
- "self",
15471
- "tenant",
15472
- "context",
15473
- "scope",
15474
- "versions",
15475
- "lookup"
15476
- ]);
15477
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
15478
15752
  function isRecognizedOwnershipKey(key) {
15753
+ if (key === DIM_WILDCARD) return true;
15479
15754
  if (key === "userId" || key === "partnerUserId") return true;
15480
15755
  if (!key.startsWith("scope:")) return false;
15481
15756
  const ns = key.slice("scope:".length);
15482
15757
  return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
15483
15758
  }
15484
- var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
15759
+ var DIM_WILDCARD = "*";
15760
+ var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
15485
15761
  function normalizeClauses(raw) {
15486
15762
  const out = [];
15487
15763
  for (const c of raw) {
@@ -15497,7 +15773,9 @@ function normalizeClauses(raw) {
15497
15773
  dataScope[dim] = values.map((val) => {
15498
15774
  if (val === null) return null;
15499
15775
  const s = String(val);
15500
- return SELF_TOKEN_RE2.test(s) ? SELF : s;
15776
+ if (ANY_TOKEN_RE.test(s)) return ANY;
15777
+ if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
15778
+ return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
15501
15779
  });
15502
15780
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
15503
15781
  }
@@ -15505,6 +15783,38 @@ function normalizeClauses(raw) {
15505
15783
  }
15506
15784
  return out;
15507
15785
  }
15786
+ function recordTypesOf(actions) {
15787
+ if (actions.includes("*")) return "all";
15788
+ const types = /* @__PURE__ */ new Set();
15789
+ let sawRecordsAction = false;
15790
+ let sawUntyped = false;
15791
+ for (const action of actions) {
15792
+ const parts = action.split(":");
15793
+ if (parts[0] !== "records") continue;
15794
+ if (!(parts[1] ?? "").includes("r")) continue;
15795
+ if (parts.length >= 3) {
15796
+ const type = parts.slice(2).join(":");
15797
+ if (type === "") continue;
15798
+ if (type === "*") continue;
15799
+ if (type.endsWith(":*")) {
15800
+ const stripped = type.slice(0, -2);
15801
+ if (stripped === "" || stripped.includes(":")) continue;
15802
+ sawRecordsAction = true;
15803
+ types.add(stripped);
15804
+ continue;
15805
+ }
15806
+ if (type.includes(":")) continue;
15807
+ sawRecordsAction = true;
15808
+ types.add(type);
15809
+ } else {
15810
+ sawRecordsAction = true;
15811
+ sawUntyped = true;
15812
+ }
15813
+ }
15814
+ if (!sawRecordsAction) return null;
15815
+ if (sawUntyped) return "all";
15816
+ return [...types].sort();
15817
+ }
15508
15818
  function buildArchetypes(clauses) {
15509
15819
  const rows = [
15510
15820
  { label: "a record you own alone (private)", owners: { user: SELF } },
@@ -15515,8 +15825,13 @@ function buildArchetypes(clauses) {
15515
15825
  if (c.unrecognizedDims.length > 0) continue;
15516
15826
  for (const [dim, values] of Object.entries(c.dataScope)) {
15517
15827
  if (dim === "user") continue;
15828
+ if (dim === DIM_WILDCARD) continue;
15518
15829
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
15519
- for (const v of values) if (v !== null) set.add(v);
15830
+ set.add(SELF);
15831
+ for (const v of values) {
15832
+ if (v === null || v === ANY || v === UNDER_SELF) continue;
15833
+ set.add(v);
15834
+ }
15520
15835
  scopeDims.set(dim, set);
15521
15836
  }
15522
15837
  }
@@ -15535,19 +15850,42 @@ function buildArchetypes(clauses) {
15535
15850
  rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
15536
15851
  return rows;
15537
15852
  }
15853
+ function listAdmits(allowed, recordValue) {
15854
+ return allowed.some((a) => {
15855
+ if (a === null) return recordValue === void 0;
15856
+ if (a === SELF) return recordValue === SELF;
15857
+ if (a === ANY) return recordValue !== void 0;
15858
+ if (a === UNDER_SELF) return false;
15859
+ return recordValue === a;
15860
+ });
15861
+ }
15538
15862
  function clauseMatches(clause, arch) {
15539
15863
  if (clause.unrecognizedDims.length > 0) return false;
15864
+ const wildcard = clause.dataScope[DIM_WILDCARD];
15540
15865
  for (const [dim, allowed] of Object.entries(clause.dataScope)) {
15541
- const recordValue = arch.owners[dim];
15542
- const ok = allowed.some((a) => {
15543
- if (a === null) return recordValue === void 0;
15544
- if (a === SELF) return recordValue === SELF;
15545
- return recordValue === a;
15546
- });
15547
- if (!ok) return false;
15866
+ if (dim === DIM_WILDCARD) continue;
15867
+ if (!listAdmits(allowed, arch.owners[dim])) return false;
15868
+ }
15869
+ if (wildcard) {
15870
+ let carriesAny = false;
15871
+ for (const [dim, value] of Object.entries(arch.owners)) {
15872
+ carriesAny = true;
15873
+ if (dim in clause.dataScope) continue;
15874
+ if (!listAdmits(wildcard, value)) return false;
15875
+ }
15876
+ if (!carriesAny && !listAdmits(wildcard, void 0)) return false;
15548
15877
  }
15549
15878
  return true;
15550
15879
  }
15880
+ function undecidableDims(clauses) {
15881
+ const dims = /* @__PURE__ */ new Set();
15882
+ for (const c of clauses) {
15883
+ for (const [dim, values] of Object.entries(c.dataScope)) {
15884
+ if (values.includes(UNDER_SELF)) dims.add(dim);
15885
+ }
15886
+ }
15887
+ return [...dims];
15888
+ }
15551
15889
  function explainClauses(clauses) {
15552
15890
  const archetypes = buildArchetypes(clauses);
15553
15891
  return archetypes.map((arch) => {
@@ -15559,15 +15897,34 @@ function explainClauses(clauses) {
15559
15897
  for (const a of c.actions) actions.add(a);
15560
15898
  }
15561
15899
  });
15562
- return { archetype: arch.label, clauses: matched, actions: [...actions] };
15900
+ const recordTypes = recordTypesOf([...actions]);
15901
+ return {
15902
+ archetype: arch.label,
15903
+ clauses: matched,
15904
+ actions: [...actions],
15905
+ ...recordTypes !== null ? { recordTypes } : {}
15906
+ };
15563
15907
  });
15564
15908
  }
15565
15909
  function renderDataScope(ds) {
15566
15910
  const entries = Object.entries(ds);
15567
15911
  if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
15568
- return entries.map(
15569
- ([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
15570
- ).join(" AND ");
15912
+ return entries.map(([dim, values]) => {
15913
+ const shown = values.map((v) => {
15914
+ if (v === SELF) return "self";
15915
+ if (v === ANY) return "any";
15916
+ if (v === UNDER_SELF) return "under-self";
15917
+ if (v === null) return "null";
15918
+ return v;
15919
+ });
15920
+ const label = dim === DIM_WILDCARD ? "* (every other dimension)" : dim;
15921
+ return `${label}:[${shown.join(", ")}]`;
15922
+ }).join(" AND ");
15923
+ }
15924
+ function renderRecordTypes(recordTypes) {
15925
+ if (recordTypes === void 0) return "\u2014";
15926
+ if (recordTypes === "all") return "all types";
15927
+ return recordTypes.join(", ");
15571
15928
  }
15572
15929
  var SELF_PRINCIPAL_ALIAS2 = "me";
15573
15930
  async function runAccessExplain(opts, deps = {}) {
@@ -15595,6 +15952,7 @@ async function runAccessExplain(opts, deps = {}) {
15595
15952
  }
15596
15953
  const clauses = normalizeClauses(rawClauses);
15597
15954
  const rows = explainClauses(clauses);
15955
+ const undecidable = undecidableDims(clauses);
15598
15956
  if (opts.json) {
15599
15957
  process.stdout.write(
15600
15958
  `${formatJson({
@@ -15611,7 +15969,10 @@ async function runAccessExplain(opts, deps = {}) {
15611
15969
  ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
15612
15970
  })),
15613
15971
  matrix: rows,
15614
- 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."
15972
+ // Present only when a clause uses `${{ under.self.* }}`: those rows are UNDECIDED
15973
+ // offline, not denied, and a consumer reading a blank cell as a denial would be wrong.
15974
+ ...undecidable.length > 0 ? { undecidableDims: undecidable } : {},
15975
+ 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."
15615
15976
  })}
15616
15977
  `
15617
15978
  );
@@ -15639,8 +16000,19 @@ async function runAccessExplain(opts, deps = {}) {
15639
16000
  "\n" + formatTable(rows, [
15640
16001
  { header: "record owned by\u2026", get: (r) => r.archetype },
15641
16002
  { header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
15642
- { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
15643
- ]) + "\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"
16003
+ { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" },
16004
+ // The ownership "granted?" cell says nothing about WHICH record types — a clause can be
16005
+ // ownership-unscoped and still type-restricted (a curated shared-KB clause with no owner
16006
+ // dimension, readable only for a few named types, never others). Surface the actual
16007
+ // `records:` reach per row instead of leaving it to a footnote.
16008
+ { header: "record types", get: (r) => renderRecordTypes(r.recordTypes) }
16009
+ ]) + (undecidable.length > 0 ? `
16010
+
16011
+ \u26A0 UNDECIDED offline: ${undecidable.join(", ")}
16012
+ A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
16013
+ so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
16014
+ NOT as a denial \u2014 the matrix under-reports this clause.
16015
+ ` : "") + '\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'
15644
16016
  );
15645
16017
  return 0;
15646
16018
  } catch (err) {
@@ -18379,9 +18751,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
18379
18751
 
18380
18752
  // src/build-info.ts
18381
18753
  var BUILD_INFO = {
18382
- cli: true ? "0.12.0" : "dev",
18383
- sdk: true ? "0.37.0" : "dev",
18384
- blueprints: true ? "0.9.0" : "dev"
18754
+ cli: true ? "0.13.0" : "dev",
18755
+ sdk: true ? "0.38.0" : "dev",
18756
+ blueprints: true ? "0.10.0" : "dev"
18385
18757
  };
18386
18758
  function formatBuildInfo() {
18387
18759
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;