@loomcycle/client 1.121.0 → 1.122.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/README.md +50 -2
- package/dist/cjs/client.js +174 -2
- package/dist/cjs/errors.js +29 -1
- package/dist/cjs/fetch-helpers.js +49 -0
- package/dist/cjs/index.js +14 -2
- package/dist/client.d.ts +173 -3
- package/dist/client.js +174 -2
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +27 -0
- package/dist/fetch-helpers.d.ts +2 -0
- package/dist/fetch-helpers.js +50 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.js +13 -2
- package/dist/types.d.ts +480 -5
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { InteractiveSession } from "./interactive.js";
|
|
27
27
|
import { ClientToolHost, type ConnectClientToolsOptions } from "./client-tools.js";
|
|
28
|
-
import type { ChangeFeedFrame, StreamChangesOptions, CredentialListResponse, CredentialMeta, CryptoKeyEncoding, CryptoKeyExport, CryptoKeyExternal, CryptoKeyType, CryptoRequest, CryptoResultByOp, DecideRequest, DecideResponse, DecisionModelsResponse, CredentialScope, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResult, Agent, AgentEvent, AgentList, CancelAgentResult, ClientOptions, ContinueOptions, RunOverrideOptions, EffectiveConfigResponse, AgentOverridableResponse, RetuneRunResponse, RunConfigResponse, RunPromptResponse, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureHookDefOptions, EnsureHookDefResult, HookDefInput, HookDefOverlay, HookDefRowResponse, CreateDefOptions, ForkDefOptions, HookDefForkOverlay, ScheduleDefInput, ScheduleDefOverlay, ScheduleDefRowResponse, WebhookDefInput, WebhookDefOverlay, WebhookDefRowResponse, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, InterruptListResponse, InterruptStatus, ListUserAgentsOptions, WalkRunsPage, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ListTeamChannelsOptions, PeekTeamChannelOptions, ReleaseTeamChannelOptions, TeamChannelPeekResult, TeamChannelReleaseResult, TeamChannelsResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, ReleaseChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, ListUserTokensResponse, RunnableAgentsResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEmbedStatsResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryBackfillResponse, MemoryPurgeResponse, MemoryReembedResponse, MemoryScopeIDsResponse, MemoryScopesResponse, MemorySearchInput, MemorySearchResponse, PauseResult, RetentionOverride, PersistentVolumesResponse, EphemeralVolumesResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, RunTreeDeletion, ReplaySessionResult, CancelTurnResult, ReviewRunResult, RunBatchOptions, RunBatchResult, RunOptions, ConfiguredRun, ConfiguredRunPatch, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, CreatedTeam, CreateTeamOptions, DefNamesResponse, ForkTeamOptions, ModelAliasesResponse, OperatorTokenDefNamesResponse, ProviderModelsResponse, RoutingResponse, ScheduleListResponse, SchedulePauseResult, ScheduleRunNowResult, ScheduleStateView, ListTeamsResponse, PromotedTeam, RetiredTeam, TeamBreakpoints, TeamDefDetail, TeamRunDetached, TeamRunTarget, TeamVerification, TeamDraft, TeamDraftVerification, TeamVersionList, TeamDiagram, TeamRunResult, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, EvaluationSubmitInput, EvaluationGetInput, EvaluationListForRunInput, EvaluationListForDefInput, EvaluationAggregateInput, EvaluationInput, EvaluationSubmitResult, EvaluationRecord, EvaluationListForRunResult, EvaluationListForDefResult, EvaluationAggregate, TranscriptResponse, WhoamiResponse, UsageDimension, UsageReportResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, SetTokenLimitRequest } from "./types.js";
|
|
28
|
+
import type { ChangeFeedFrame, StreamChangesOptions, CredentialListResponse, CredentialMeta, CryptoKeyEncoding, CryptoKeyExport, CryptoKeyExternal, CryptoKeyType, CryptoRequest, CryptoResultByOp, DecideRequest, DecideResponse, DecisionModelsResponse, CredentialScope, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResult, Agent, AgentEvent, AgentList, CancelAgentResult, ClientOptions, ContinueOptions, RunOverrideOptions, EffectiveConfigResponse, AgentOverridableResponse, RetuneRunResponse, RunConfigResponse, RunPromptResponse, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureHookDefOptions, EnsureHookDefResult, HookDefInput, HookDefOverlay, HookDefRowResponse, CreateDefOptions, ForkDefOptions, HookDefForkOverlay, ScheduleDefInput, ScheduleDefOverlay, ScheduleDefRowResponse, WebhookDefInput, WebhookDefOverlay, WebhookDefRowResponse, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, InterruptListResponse, InterruptStatus, ListUserAgentsOptions, WalkRunsPage, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ListTeamChannelsOptions, PeekTeamChannelOptions, ReleaseTeamChannelOptions, TeamChannelPeekResult, TeamChannelReleaseResult, TeamChannelsResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, ReleaseChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, ListUserTokensResponse, RunnableAgentsResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEmbedStatsResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryBackfillResponse, MemoryPurgeResponse, MemoryReembedResponse, MemoryScopeIDsResponse, MemoryScopesResponse, MemorySearchInput, MemorySearchResponse, PauseResult, RetentionOverride, PersistentVolumesResponse, EphemeralVolumesResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, RunTreeDeletion, ReplaySessionResult, CancelTurnResult, ReviewRunResult, RunBatchOptions, RunBatchResult, RunOptions, ConfiguredRun, ConfiguredRunPatch, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, CreatedTeam, CreateTeamOptions, DefNamesResponse, ForkTeamOptions, ModelAliasesResponse, OperatorTokenDefNamesResponse, ProviderModelsResponse, RoutingResponse, ScheduleListResponse, SchedulePauseResult, ScheduleRunNowResult, ScheduleStateView, ListTeamsResponse, PromotedTeam, RetiredTeam, TeamBreakpoints, TeamDefDetail, TeamRunDetached, TeamRunTarget, TeamVerification, TeamDraft, TeamDraftVerification, TeamVersionList, TeamDiagram, TeamRunResult, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, EvaluationSubmitInput, EvaluationGetInput, EvaluationListForRunInput, EvaluationListForDefInput, EvaluationAggregateInput, EvaluationInput, EvaluationSubmitResult, EvaluationRecord, EvaluationListForRunResult, EvaluationListForDefResult, EvaluationAggregate, TranscriptResponse, WhoamiResponse, UsageDimension, UsageReportResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, SetTokenLimitRequest, AclBinding, AclBindingListResponse, AclCatalog, AclDefInput, AclDefListResponse, AclDefNamesResponse, AclDefRow, AclRule, AclValidateResult, AuthzDecision, AuthzEvaluationRequest, AuthzEvaluationsRequest, AuthzEvaluationsResponse, AuthzExplainResponse, AuthzSimulateRequest, AuthzSimulateResponse, CreateAclBindingBody, CreateGroupBody, GroupDetail, GroupEffective, GroupListResponse, GroupMemberChange, GroupMemberKind, GroupRow } from "./types.js";
|
|
29
29
|
export declare class LoomcycleClient {
|
|
30
30
|
private ctx;
|
|
31
31
|
constructor(opts?: ClientOptions);
|
|
@@ -1288,14 +1288,184 @@ export declare class LoomcycleClient {
|
|
|
1288
1288
|
tenant?: string;
|
|
1289
1289
|
signal?: AbortSignal;
|
|
1290
1290
|
}): Promise<DefNamesResponse>;
|
|
1291
|
-
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`),
|
|
1292
|
-
*
|
|
1291
|
+
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), one row
|
|
1292
|
+
* per (tenant, name), with its subject, token counts and current def_id —
|
|
1293
|
+
* never a token. **Admin token only**: token
|
|
1293
1294
|
* minting stays operator-only, so a `substrate:tenant` token is refused
|
|
1294
1295
|
* with 403. */
|
|
1295
1296
|
listOperatorTokenDefNames(opts?: {
|
|
1296
1297
|
tenant?: string;
|
|
1297
1298
|
signal?: AbortSignal;
|
|
1298
1299
|
}): Promise<OperatorTokenDefNamesResponse>;
|
|
1300
|
+
/** `path` with the admin tenant focus and any extra query parameters. */
|
|
1301
|
+
private aclPath;
|
|
1302
|
+
/** Every group of the tenant, stored and config-declared, sorted by name
|
|
1303
|
+
* (`GET /v1/_groups`). */
|
|
1304
|
+
listGroups(opts?: {
|
|
1305
|
+
tenant?: string;
|
|
1306
|
+
signal?: AbortSignal;
|
|
1307
|
+
}): Promise<GroupListResponse>;
|
|
1308
|
+
/** One stored group with its direct members and the groups it is inside of
|
|
1309
|
+
* (`GET /v1/_groups/{name}`). A config-only group is not stored and raises
|
|
1310
|
+
* NotFoundError; {@link LoomcycleClient.getGroupEffective} covers it. */
|
|
1311
|
+
getGroup(name: string, opts?: {
|
|
1312
|
+
tenant?: string;
|
|
1313
|
+
signal?: AbortSignal;
|
|
1314
|
+
}): Promise<GroupDetail>;
|
|
1315
|
+
/** Every user the group holds, through nested groups at any depth and
|
|
1316
|
+
* config-declared members included (`GET /v1/_groups/{name}/effective`). */
|
|
1317
|
+
getGroupEffective(name: string, opts?: {
|
|
1318
|
+
tenant?: string;
|
|
1319
|
+
signal?: AbortSignal;
|
|
1320
|
+
}): Promise<GroupEffective>;
|
|
1321
|
+
/** Create a group, or update its description and `external_id` if the name
|
|
1322
|
+
* exists (`POST /v1/_groups`). A group with an `external_id` is owned by an
|
|
1323
|
+
* identity provider: a later write to it raises a 409 LoomcycleError with
|
|
1324
|
+
* code `group_external`. */
|
|
1325
|
+
createGroup(body: CreateGroupBody, opts?: {
|
|
1326
|
+
tenant?: string;
|
|
1327
|
+
signal?: AbortSignal;
|
|
1328
|
+
}): Promise<GroupRow>;
|
|
1329
|
+
/** Delete a group and every membership edge naming it
|
|
1330
|
+
* (`DELETE /v1/_groups/{name}`). Bindings to it stay and apply to no one.
|
|
1331
|
+
* NotFoundError when there is no such group. */
|
|
1332
|
+
deleteGroup(name: string, opts?: {
|
|
1333
|
+
tenant?: string;
|
|
1334
|
+
signal?: AbortSignal;
|
|
1335
|
+
}): Promise<{
|
|
1336
|
+
deleted: string;
|
|
1337
|
+
}>;
|
|
1338
|
+
/** Add a user or a group to a group (`POST /v1/_groups/{name}/members`). A
|
|
1339
|
+
* group member nests: its members are in this group too. The server
|
|
1340
|
+
* refuses an edge that would close a cycle or nest too deep (400), one
|
|
1341
|
+
* that exists (409 `group_member_exists`), and a delegated writer adding
|
|
1342
|
+
* itself (403 `acl_self_elevation`). */
|
|
1343
|
+
addGroupMember(name: string, member: {
|
|
1344
|
+
kind: GroupMemberKind;
|
|
1345
|
+
id: string;
|
|
1346
|
+
}, opts?: {
|
|
1347
|
+
tenant?: string;
|
|
1348
|
+
signal?: AbortSignal;
|
|
1349
|
+
}): Promise<GroupMemberChange>;
|
|
1350
|
+
/** Remove one membership edge
|
|
1351
|
+
* (`DELETE /v1/_groups/{name}/members/{kind}/{id}`). NotFoundError when
|
|
1352
|
+
* there is no such edge. */
|
|
1353
|
+
removeGroupMember(name: string, kind: GroupMemberKind, id: string, opts?: {
|
|
1354
|
+
tenant?: string;
|
|
1355
|
+
signal?: AbortSignal;
|
|
1356
|
+
}): Promise<GroupMemberChange>;
|
|
1357
|
+
/** The AclDef family, op-dispatched — the same ops the gRPC `AclDef` RPC
|
|
1358
|
+
* and the MCP `acldef` tool take. Every op but `names` posts to
|
|
1359
|
+
* `POST /v1/_acldef`; `names` reads `GET /v1/_acldef/names`, which is
|
|
1360
|
+
* where that op lives on HTTP. The response varies by op; the typed
|
|
1361
|
+
* helpers below return each op's shape. */
|
|
1362
|
+
aclDef(input: AclDefInput, opts?: {
|
|
1363
|
+
tenant?: string;
|
|
1364
|
+
signal?: AbortSignal;
|
|
1365
|
+
}): Promise<unknown>;
|
|
1366
|
+
/** Every rule list of the tenant, stored and config-declared, plus the
|
|
1367
|
+
* built-in baselines (`aclDef` op `list`). Retired lists only with
|
|
1368
|
+
* `includeRetired`. */
|
|
1369
|
+
listAclDefs(opts?: {
|
|
1370
|
+
includeRetired?: boolean;
|
|
1371
|
+
tenant?: string;
|
|
1372
|
+
signal?: AbortSignal;
|
|
1373
|
+
}): Promise<AclDefListResponse>;
|
|
1374
|
+
/** One rule list, retired or not (`aclDef` op `get`). A config-declared
|
|
1375
|
+
* list is returned with `source: "config"`. NotFoundError when absent. */
|
|
1376
|
+
getAclDef(name: string, opts?: {
|
|
1377
|
+
tenant?: string;
|
|
1378
|
+
signal?: AbortSignal;
|
|
1379
|
+
}): Promise<AclDefRow>;
|
|
1380
|
+
/** Create or replace a rule list by name (`aclDef` op `put`): bumps its
|
|
1381
|
+
* version and un-retires it. The rules are validated first; a list the
|
|
1382
|
+
* engine could not enforce is refused with 400. A delegated writer may
|
|
1383
|
+
* not rewrite a list bound to itself (403 `acl_self_elevation`). */
|
|
1384
|
+
putAclDef(body: {
|
|
1385
|
+
name: string;
|
|
1386
|
+
rules: AclRule[];
|
|
1387
|
+
description?: string;
|
|
1388
|
+
}, opts?: {
|
|
1389
|
+
tenant?: string;
|
|
1390
|
+
signal?: AbortSignal;
|
|
1391
|
+
}): Promise<AclDefRow>;
|
|
1392
|
+
/** Retire a rule list (`aclDef` op `retire`). Bindings to it stay and match
|
|
1393
|
+
* nothing; a later `putAclDef` of the name brings it back. NotFoundError
|
|
1394
|
+
* when there is no live list of that name. */
|
|
1395
|
+
retireAclDef(name: string, opts?: {
|
|
1396
|
+
tenant?: string;
|
|
1397
|
+
signal?: AbortSignal;
|
|
1398
|
+
}): Promise<{
|
|
1399
|
+
retired: string;
|
|
1400
|
+
}>;
|
|
1401
|
+
/** Check rules without storing them (`aclDef` op `validate`). Rules the
|
|
1402
|
+
* engine refuses raise InvalidArgumentError (code `acl_invalid`) naming
|
|
1403
|
+
* the fault. */
|
|
1404
|
+
validateAclRules(rules: AclRule[], opts?: {
|
|
1405
|
+
tenant?: string;
|
|
1406
|
+
signal?: AbortSignal;
|
|
1407
|
+
}): Promise<AclValidateResult>;
|
|
1408
|
+
/** The resource types, verbs and transports a rule may name (`aclDef` op
|
|
1409
|
+
* `catalog`). */
|
|
1410
|
+
getAclCatalog(opts?: {
|
|
1411
|
+
tenant?: string;
|
|
1412
|
+
signal?: AbortSignal;
|
|
1413
|
+
}): Promise<AclCatalog>;
|
|
1414
|
+
/** Every live rule-list name a binding in the tenant can reach, stored and
|
|
1415
|
+
* config-declared, sorted (`GET /v1/_acldef/names`). */
|
|
1416
|
+
listAclDefNames(opts?: {
|
|
1417
|
+
tenant?: string;
|
|
1418
|
+
signal?: AbortSignal;
|
|
1419
|
+
}): Promise<AclDefNamesResponse>;
|
|
1420
|
+
/** Every binding of the tenant, stored and config-declared
|
|
1421
|
+
* (`GET /v1/_aclbinding`). Retired ones only with `includeRetired`. */
|
|
1422
|
+
listAclBindings(opts?: {
|
|
1423
|
+
includeRetired?: boolean;
|
|
1424
|
+
tenant?: string;
|
|
1425
|
+
signal?: AbortSignal;
|
|
1426
|
+
}): Promise<AclBindingListResponse>;
|
|
1427
|
+
/** Bind a rule list to a subject (`POST /v1/_aclbinding`). The list must
|
|
1428
|
+
* exist and `expires_at` must be in the future, or InvalidArgumentError
|
|
1429
|
+
* (code `acl_binding_invalid`). A delegated writer may not bind itself, a
|
|
1430
|
+
* group it is in, its token or its tenant (403 `acl_self_elevation`). An
|
|
1431
|
+
* empty `subject_id` / `expires_at` is left out of the request. */
|
|
1432
|
+
createAclBinding(body: CreateAclBindingBody, opts?: {
|
|
1433
|
+
tenant?: string;
|
|
1434
|
+
signal?: AbortSignal;
|
|
1435
|
+
}): Promise<AclBinding>;
|
|
1436
|
+
/** Retire a binding by its `binding_id` (`DELETE /v1/_aclbinding/{id}`).
|
|
1437
|
+
* NotFoundError when there is no live binding of that id. A config
|
|
1438
|
+
* binding has no id and cannot be retired here. */
|
|
1439
|
+
deleteAclBinding(bindingId: string, opts?: {
|
|
1440
|
+
tenant?: string;
|
|
1441
|
+
signal?: AbortSignal;
|
|
1442
|
+
}): Promise<{
|
|
1443
|
+
retired: string;
|
|
1444
|
+
}>;
|
|
1445
|
+
/** May the subject do this? (`POST /v1/_authz/evaluation`, AuthZEN 1.0).
|
|
1446
|
+
* Omit `subject` to ask about yourself. */
|
|
1447
|
+
authzEvaluation(req: AuthzEvaluationRequest, opts?: {
|
|
1448
|
+
tenant?: string;
|
|
1449
|
+
signal?: AbortSignal;
|
|
1450
|
+
}): Promise<AuthzDecision>;
|
|
1451
|
+
/** Up to 256 questions for one subject in one call
|
|
1452
|
+
* (`POST /v1/_authz/evaluations`); decisions come back in order. */
|
|
1453
|
+
authzEvaluations(req: AuthzEvaluationsRequest, opts?: {
|
|
1454
|
+
tenant?: string;
|
|
1455
|
+
signal?: AbortSignal;
|
|
1456
|
+
}): Promise<AuthzEvaluationsResponse>;
|
|
1457
|
+
/** A decision with its working: every rule read for the resource's type
|
|
1458
|
+
* and why it did or did not match (`POST /v1/_authz/explain`). */
|
|
1459
|
+
authzExplain(req: AuthzEvaluationRequest, opts?: {
|
|
1460
|
+
tenant?: string;
|
|
1461
|
+
signal?: AbortSignal;
|
|
1462
|
+
}): Promise<AuthzExplainResponse>;
|
|
1463
|
+
/** What a draft of lists and bindings would decide, beside what is decided
|
|
1464
|
+
* now (`POST /v1/_authz/simulate`). Nothing is written. */
|
|
1465
|
+
authzSimulate(req: AuthzSimulateRequest, opts?: {
|
|
1466
|
+
tenant?: string;
|
|
1467
|
+
signal?: AbortSignal;
|
|
1468
|
+
}): Promise<AuthzSimulateResponse>;
|
|
1299
1469
|
/** Every schedule: static yaml `scheduled_runs` and ScheduleDefs, merged by
|
|
1300
1470
|
* name (`GET /v1/_schedules/list-all`), sorted by name. A tenant token sees
|
|
1301
1471
|
* only its own tenant's ScheduleDefs — static schedules are operator
|
package/dist/client.js
CHANGED
|
@@ -1496,6 +1496,8 @@ export class LoomcycleClient {
|
|
|
1496
1496
|
};
|
|
1497
1497
|
if (opts.headers)
|
|
1498
1498
|
overlay.headers = opts.headers;
|
|
1499
|
+
if (opts.binaryResults)
|
|
1500
|
+
overlay.binary_results = opts.binaryResults;
|
|
1499
1501
|
const createInput = { op: "create", name: opts.name, overlay };
|
|
1500
1502
|
if (opts.description)
|
|
1501
1503
|
createInput.description = opts.description;
|
|
@@ -1821,13 +1823,183 @@ export class LoomcycleClient {
|
|
|
1821
1823
|
async listDocumentSourceDefNames(opts) {
|
|
1822
1824
|
return this.listDefNames("documentsourcedef", opts);
|
|
1823
1825
|
}
|
|
1824
|
-
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`),
|
|
1825
|
-
*
|
|
1826
|
+
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), one row
|
|
1827
|
+
* per (tenant, name), with its subject, token counts and current def_id —
|
|
1828
|
+
* never a token. **Admin token only**: token
|
|
1826
1829
|
* minting stays operator-only, so a `substrate:tenant` token is refused
|
|
1827
1830
|
* with 403. */
|
|
1828
1831
|
async listOperatorTokenDefNames(opts) {
|
|
1829
1832
|
return this.listDefNames("operatortokendef", opts);
|
|
1830
1833
|
}
|
|
1834
|
+
// ---- Access control: groups, rule lists, bindings, decisions (RFC EY / EZ) ----
|
|
1835
|
+
//
|
|
1836
|
+
// Groups, lists and bindings are the tenant operator's: a `substrate:tenant`
|
|
1837
|
+
// token, an admin, or the legacy shared token. Any other principal is
|
|
1838
|
+
// refused with 403 (InsufficientScopeError). Every call acts in the caller's
|
|
1839
|
+
// own tenant; `tenant` is an admin's focus on another one and is ignored for
|
|
1840
|
+
// a tenant operator. Rows the config declares are listed beside the stored
|
|
1841
|
+
// ones with `source: "config"`; they cannot be written here. Every write
|
|
1842
|
+
// takes effect on every replica at once. A rule that refuses a call throws
|
|
1843
|
+
// AclDeniedError (403 `acl_denied`) naming the rules that decided.
|
|
1844
|
+
//
|
|
1845
|
+
// The decision questions (authzEvaluation / authzEvaluations / authzExplain
|
|
1846
|
+
// / authzSimulate) are open to any principal about itself; naming another
|
|
1847
|
+
// subject of the tenant needs a tenant operator (403 `authz_subject`).
|
|
1848
|
+
// Every call here needs a persistent store (503 otherwise) and is refused
|
|
1849
|
+
// from inside a run.
|
|
1850
|
+
/** `path` with the admin tenant focus and any extra query parameters. */
|
|
1851
|
+
aclPath(path, tenant, extra) {
|
|
1852
|
+
const q = new URLSearchParams(extra);
|
|
1853
|
+
if (tenant)
|
|
1854
|
+
q.set("tenant", tenant);
|
|
1855
|
+
const qs = q.toString();
|
|
1856
|
+
return qs ? `${path}?${qs}` : path;
|
|
1857
|
+
}
|
|
1858
|
+
/** Every group of the tenant, stored and config-declared, sorted by name
|
|
1859
|
+
* (`GET /v1/_groups`). */
|
|
1860
|
+
async listGroups(opts) {
|
|
1861
|
+
return jsonFetch(this.ctx, this.aclPath("/v1/_groups", opts?.tenant), opts);
|
|
1862
|
+
}
|
|
1863
|
+
/** One stored group with its direct members and the groups it is inside of
|
|
1864
|
+
* (`GET /v1/_groups/{name}`). A config-only group is not stored and raises
|
|
1865
|
+
* NotFoundError; {@link LoomcycleClient.getGroupEffective} covers it. */
|
|
1866
|
+
async getGroup(name, opts) {
|
|
1867
|
+
return jsonFetch(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}`, opts?.tenant), opts);
|
|
1868
|
+
}
|
|
1869
|
+
/** Every user the group holds, through nested groups at any depth and
|
|
1870
|
+
* config-declared members included (`GET /v1/_groups/{name}/effective`). */
|
|
1871
|
+
async getGroupEffective(name, opts) {
|
|
1872
|
+
return jsonFetch(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}/effective`, opts?.tenant), opts);
|
|
1873
|
+
}
|
|
1874
|
+
/** Create a group, or update its description and `external_id` if the name
|
|
1875
|
+
* exists (`POST /v1/_groups`). A group with an `external_id` is owned by an
|
|
1876
|
+
* identity provider: a later write to it raises a 409 LoomcycleError with
|
|
1877
|
+
* code `group_external`. */
|
|
1878
|
+
async createGroup(body, opts) {
|
|
1879
|
+
return postJSON(this.ctx, this.aclPath("/v1/_groups", opts?.tenant), body, opts);
|
|
1880
|
+
}
|
|
1881
|
+
/** Delete a group and every membership edge naming it
|
|
1882
|
+
* (`DELETE /v1/_groups/{name}`). Bindings to it stay and apply to no one.
|
|
1883
|
+
* NotFoundError when there is no such group. */
|
|
1884
|
+
async deleteGroup(name, opts) {
|
|
1885
|
+
return deleteJSON(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}`, opts?.tenant), opts);
|
|
1886
|
+
}
|
|
1887
|
+
/** Add a user or a group to a group (`POST /v1/_groups/{name}/members`). A
|
|
1888
|
+
* group member nests: its members are in this group too. The server
|
|
1889
|
+
* refuses an edge that would close a cycle or nest too deep (400), one
|
|
1890
|
+
* that exists (409 `group_member_exists`), and a delegated writer adding
|
|
1891
|
+
* itself (403 `acl_self_elevation`). */
|
|
1892
|
+
async addGroupMember(name, member, opts) {
|
|
1893
|
+
return postJSON(this.ctx, this.aclPath(`/v1/_groups/${encodeURIComponent(name)}/members`, opts?.tenant), { kind: member.kind, id: member.id }, opts);
|
|
1894
|
+
}
|
|
1895
|
+
/** Remove one membership edge
|
|
1896
|
+
* (`DELETE /v1/_groups/{name}/members/{kind}/{id}`). NotFoundError when
|
|
1897
|
+
* there is no such edge. */
|
|
1898
|
+
async removeGroupMember(name, kind, id, opts) {
|
|
1899
|
+
const path = `/v1/_groups/${encodeURIComponent(name)}/members/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`;
|
|
1900
|
+
return deleteJSON(this.ctx, this.aclPath(path, opts?.tenant), opts);
|
|
1901
|
+
}
|
|
1902
|
+
/** The AclDef family, op-dispatched — the same ops the gRPC `AclDef` RPC
|
|
1903
|
+
* and the MCP `acldef` tool take. Every op but `names` posts to
|
|
1904
|
+
* `POST /v1/_acldef`; `names` reads `GET /v1/_acldef/names`, which is
|
|
1905
|
+
* where that op lives on HTTP. The response varies by op; the typed
|
|
1906
|
+
* helpers below return each op's shape. */
|
|
1907
|
+
async aclDef(input, opts) {
|
|
1908
|
+
if (input.op === "names")
|
|
1909
|
+
return this.listAclDefNames(opts);
|
|
1910
|
+
return postJSON(this.ctx, this.aclPath("/v1/_acldef", opts?.tenant), input, opts);
|
|
1911
|
+
}
|
|
1912
|
+
/** Every rule list of the tenant, stored and config-declared, plus the
|
|
1913
|
+
* built-in baselines (`aclDef` op `list`). Retired lists only with
|
|
1914
|
+
* `includeRetired`. */
|
|
1915
|
+
async listAclDefs(opts) {
|
|
1916
|
+
const input = { op: "list" };
|
|
1917
|
+
if (opts?.includeRetired)
|
|
1918
|
+
input.include_retired = true;
|
|
1919
|
+
return (await this.aclDef(input, opts));
|
|
1920
|
+
}
|
|
1921
|
+
/** One rule list, retired or not (`aclDef` op `get`). A config-declared
|
|
1922
|
+
* list is returned with `source: "config"`. NotFoundError when absent. */
|
|
1923
|
+
async getAclDef(name, opts) {
|
|
1924
|
+
return (await this.aclDef({ op: "get", name }, opts));
|
|
1925
|
+
}
|
|
1926
|
+
/** Create or replace a rule list by name (`aclDef` op `put`): bumps its
|
|
1927
|
+
* version and un-retires it. The rules are validated first; a list the
|
|
1928
|
+
* engine could not enforce is refused with 400. A delegated writer may
|
|
1929
|
+
* not rewrite a list bound to itself (403 `acl_self_elevation`). */
|
|
1930
|
+
async putAclDef(body, opts) {
|
|
1931
|
+
const input = { op: "put", name: body.name, rules: body.rules };
|
|
1932
|
+
if (body.description !== undefined)
|
|
1933
|
+
input.description = body.description;
|
|
1934
|
+
return (await this.aclDef(input, opts));
|
|
1935
|
+
}
|
|
1936
|
+
/** Retire a rule list (`aclDef` op `retire`). Bindings to it stay and match
|
|
1937
|
+
* nothing; a later `putAclDef` of the name brings it back. NotFoundError
|
|
1938
|
+
* when there is no live list of that name. */
|
|
1939
|
+
async retireAclDef(name, opts) {
|
|
1940
|
+
return (await this.aclDef({ op: "retire", name }, opts));
|
|
1941
|
+
}
|
|
1942
|
+
/** Check rules without storing them (`aclDef` op `validate`). Rules the
|
|
1943
|
+
* engine refuses raise InvalidArgumentError (code `acl_invalid`) naming
|
|
1944
|
+
* the fault. */
|
|
1945
|
+
async validateAclRules(rules, opts) {
|
|
1946
|
+
return (await this.aclDef({ op: "validate", rules }, opts));
|
|
1947
|
+
}
|
|
1948
|
+
/** The resource types, verbs and transports a rule may name (`aclDef` op
|
|
1949
|
+
* `catalog`). */
|
|
1950
|
+
async getAclCatalog(opts) {
|
|
1951
|
+
return (await this.aclDef({ op: "catalog" }, opts));
|
|
1952
|
+
}
|
|
1953
|
+
/** Every live rule-list name a binding in the tenant can reach, stored and
|
|
1954
|
+
* config-declared, sorted (`GET /v1/_acldef/names`). */
|
|
1955
|
+
async listAclDefNames(opts) {
|
|
1956
|
+
return jsonFetch(this.ctx, this.aclPath("/v1/_acldef/names", opts?.tenant), opts);
|
|
1957
|
+
}
|
|
1958
|
+
/** Every binding of the tenant, stored and config-declared
|
|
1959
|
+
* (`GET /v1/_aclbinding`). Retired ones only with `includeRetired`. */
|
|
1960
|
+
async listAclBindings(opts) {
|
|
1961
|
+
const extra = opts?.includeRetired ? { include_retired: "1" } : undefined;
|
|
1962
|
+
return jsonFetch(this.ctx, this.aclPath("/v1/_aclbinding", opts?.tenant, extra), opts);
|
|
1963
|
+
}
|
|
1964
|
+
/** Bind a rule list to a subject (`POST /v1/_aclbinding`). The list must
|
|
1965
|
+
* exist and `expires_at` must be in the future, or InvalidArgumentError
|
|
1966
|
+
* (code `acl_binding_invalid`). A delegated writer may not bind itself, a
|
|
1967
|
+
* group it is in, its token or its tenant (403 `acl_self_elevation`). An
|
|
1968
|
+
* empty `subject_id` / `expires_at` is left out of the request. */
|
|
1969
|
+
async createAclBinding(body, opts) {
|
|
1970
|
+
const out = { acl: body.acl, subject_kind: body.subject_kind };
|
|
1971
|
+
if (body.subject_id)
|
|
1972
|
+
out.subject_id = body.subject_id;
|
|
1973
|
+
if (body.expires_at)
|
|
1974
|
+
out.expires_at = body.expires_at;
|
|
1975
|
+
return postJSON(this.ctx, this.aclPath("/v1/_aclbinding", opts?.tenant), out, opts);
|
|
1976
|
+
}
|
|
1977
|
+
/** Retire a binding by its `binding_id` (`DELETE /v1/_aclbinding/{id}`).
|
|
1978
|
+
* NotFoundError when there is no live binding of that id. A config
|
|
1979
|
+
* binding has no id and cannot be retired here. */
|
|
1980
|
+
async deleteAclBinding(bindingId, opts) {
|
|
1981
|
+
return deleteJSON(this.ctx, this.aclPath(`/v1/_aclbinding/${encodeURIComponent(bindingId)}`, opts?.tenant), opts);
|
|
1982
|
+
}
|
|
1983
|
+
/** May the subject do this? (`POST /v1/_authz/evaluation`, AuthZEN 1.0).
|
|
1984
|
+
* Omit `subject` to ask about yourself. */
|
|
1985
|
+
async authzEvaluation(req, opts) {
|
|
1986
|
+
return postJSON(this.ctx, this.aclPath("/v1/_authz/evaluation", opts?.tenant), req, opts);
|
|
1987
|
+
}
|
|
1988
|
+
/** Up to 256 questions for one subject in one call
|
|
1989
|
+
* (`POST /v1/_authz/evaluations`); decisions come back in order. */
|
|
1990
|
+
async authzEvaluations(req, opts) {
|
|
1991
|
+
return postJSON(this.ctx, this.aclPath("/v1/_authz/evaluations", opts?.tenant), req, opts);
|
|
1992
|
+
}
|
|
1993
|
+
/** A decision with its working: every rule read for the resource's type
|
|
1994
|
+
* and why it did or did not match (`POST /v1/_authz/explain`). */
|
|
1995
|
+
async authzExplain(req, opts) {
|
|
1996
|
+
return postJSON(this.ctx, this.aclPath("/v1/_authz/explain", opts?.tenant), req, opts);
|
|
1997
|
+
}
|
|
1998
|
+
/** What a draft of lists and bindings would decide, beside what is decided
|
|
1999
|
+
* now (`POST /v1/_authz/simulate`). Nothing is written. */
|
|
2000
|
+
async authzSimulate(req, opts) {
|
|
2001
|
+
return postJSON(this.ctx, this.aclPath("/v1/_authz/simulate", opts?.tenant), req, opts);
|
|
2002
|
+
}
|
|
1831
2003
|
// ---- Schedules admin (GET/POST /v1/_schedules/...) ----
|
|
1832
2004
|
//
|
|
1833
2005
|
// What the Web UI's schedules page drives: the merged list, one schedule's
|
package/dist/errors.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* that behavior. PR 5b switches `runStreaming` + every new method
|
|
18
18
|
* to raise typed errors via `raiseFromResponse`.
|
|
19
19
|
*/
|
|
20
|
+
import type { AuthzReason } from "./types.js";
|
|
20
21
|
export declare class LoomcycleError extends Error {
|
|
21
22
|
readonly status?: number;
|
|
22
23
|
readonly bodyText?: string;
|
|
@@ -253,6 +254,34 @@ export declare class InsufficientScopeError extends LoomcycleError {
|
|
|
253
254
|
requiredScope?: string;
|
|
254
255
|
});
|
|
255
256
|
}
|
|
257
|
+
/** An HTTP 403 because an access-control list refused the call: body code
|
|
258
|
+
* `acl_denied`. The token's scopes would allow it, but a rule bound to the
|
|
259
|
+
* caller (to its subject, a group it is in, its token or its tenant) denies
|
|
260
|
+
* it, or, where the deployment enforces lists, no rule allows it.
|
|
261
|
+
* `reasons` names the rules that decided — empty when none did — so an
|
|
262
|
+
* operator can find the list to change with `authzExplain`. Not retryable:
|
|
263
|
+
* the same call is refused until a list or binding changes.
|
|
264
|
+
*
|
|
265
|
+
* Other access-control refusals (`acl_self_elevation`, `authz_subject`,
|
|
266
|
+
* `operator_plane`, …) stay a plain LoomcycleError with that `code`. */
|
|
267
|
+
export declare class AclDeniedError extends LoomcycleError {
|
|
268
|
+
/** The verb asked, e.g. `read`, `author`, `manage`. Null when the body did
|
|
269
|
+
* not carry one. */
|
|
270
|
+
readonly action: string | null;
|
|
271
|
+
/** The resource asked about, `type:name` (e.g. `def:acl/ops`). Null when
|
|
272
|
+
* the body did not carry one. */
|
|
273
|
+
readonly resource: string | null;
|
|
274
|
+
/** The rules that decided, denies first; empty when no rule matched. */
|
|
275
|
+
readonly reasons: AuthzReason[];
|
|
276
|
+
constructor(message: string, opts?: {
|
|
277
|
+
status?: number;
|
|
278
|
+
bodyText?: string;
|
|
279
|
+
code?: string;
|
|
280
|
+
action?: string;
|
|
281
|
+
resource?: string;
|
|
282
|
+
reasons?: AuthzReason[];
|
|
283
|
+
});
|
|
284
|
+
}
|
|
256
285
|
/** Every HTTP 503. The status is shared between a condition that clears on
|
|
257
286
|
* its own and features this deployment does not have
|
|
258
287
|
* (`decision_not_configured`, `store_unavailable`, `vector_unsupported`, …),
|
package/dist/errors.js
CHANGED
|
@@ -268,6 +268,33 @@ export class InsufficientScopeError extends LoomcycleError {
|
|
|
268
268
|
this.requiredScope = opts?.requiredScope ?? null;
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
/** An HTTP 403 because an access-control list refused the call: body code
|
|
272
|
+
* `acl_denied`. The token's scopes would allow it, but a rule bound to the
|
|
273
|
+
* caller (to its subject, a group it is in, its token or its tenant) denies
|
|
274
|
+
* it, or, where the deployment enforces lists, no rule allows it.
|
|
275
|
+
* `reasons` names the rules that decided — empty when none did — so an
|
|
276
|
+
* operator can find the list to change with `authzExplain`. Not retryable:
|
|
277
|
+
* the same call is refused until a list or binding changes.
|
|
278
|
+
*
|
|
279
|
+
* Other access-control refusals (`acl_self_elevation`, `authz_subject`,
|
|
280
|
+
* `operator_plane`, …) stay a plain LoomcycleError with that `code`. */
|
|
281
|
+
export class AclDeniedError extends LoomcycleError {
|
|
282
|
+
/** The verb asked, e.g. `read`, `author`, `manage`. Null when the body did
|
|
283
|
+
* not carry one. */
|
|
284
|
+
action;
|
|
285
|
+
/** The resource asked about, `type:name` (e.g. `def:acl/ops`). Null when
|
|
286
|
+
* the body did not carry one. */
|
|
287
|
+
resource;
|
|
288
|
+
/** The rules that decided, denies first; empty when no rule matched. */
|
|
289
|
+
reasons;
|
|
290
|
+
constructor(message, opts) {
|
|
291
|
+
super(message, opts);
|
|
292
|
+
this.name = "AclDeniedError";
|
|
293
|
+
this.action = opts?.action ?? null;
|
|
294
|
+
this.resource = opts?.resource ?? null;
|
|
295
|
+
this.reasons = opts?.reasons ?? [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
271
298
|
/** Every HTTP 503. The status is shared between a condition that clears on
|
|
272
299
|
* its own and features this deployment does not have
|
|
273
300
|
* (`decision_not_configured`, `store_unavailable`, `vector_unsupported`, …),
|
package/dist/fetch-helpers.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export declare function deleteJSON<T>(ctx: _FetchContext, path: string, opts?: {
|
|
|
79
79
|
* 401 → AuthError
|
|
80
80
|
* 403 + code "insufficient_scope" (or an older server's
|
|
81
81
|
* WWW-Authenticate scope hint) → InsufficientScopeError
|
|
82
|
+
* 403 + code "acl_denied" → AclDeniedError (action, resource, reasons)
|
|
82
83
|
* 403 (other) → LoomcycleError (base)
|
|
83
84
|
* 404 + "snapshot" → SnapshotNotFoundError ────────┐
|
|
84
85
|
* 404 + "session" → SessionNotFoundError │ All extend
|
|
@@ -86,6 +87,7 @@ export declare function deleteJSON<T>(ctx: _FetchContext, path: string, opts?: {
|
|
|
86
87
|
* 404 + (other) → NotFoundError (base) │ catch any 404
|
|
87
88
|
* │ with one
|
|
88
89
|
* │ instanceof.
|
|
90
|
+
* 409 + an access-control code (group_external, *_exists) → LoomcycleError
|
|
89
91
|
* 409 + "already_pausing" / "already paused" → AlreadyPausingError
|
|
90
92
|
* 409 + "not_paused" / "not paused" → NotPausedError
|
|
91
93
|
* 409 + "session" → SessionBusyError
|
package/dist/fetch-helpers.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Method-level code in client.ts stays focused on URL + body shape;
|
|
10
10
|
* the boring fetch + error-translation machinery lives here.
|
|
11
11
|
*/
|
|
12
|
-
import { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, ChannelCursorRegressionError, InsufficientScopeError, InvalidArgumentError, LoomcycleError, NotFoundError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
|
|
12
|
+
import { AclDeniedError, AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, ChannelCursorRegressionError, InsufficientScopeError, InvalidArgumentError, LoomcycleError, NotFoundError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
|
|
13
13
|
/** authHeaders builds the standard request header set: JSON Accept
|
|
14
14
|
* + Bearer token when the client was constructed with one. The
|
|
15
15
|
* caller adds Content-Type when posting a body. */
|
|
@@ -159,6 +159,7 @@ export async function deleteJSON(ctx, path, opts) {
|
|
|
159
159
|
* 401 → AuthError
|
|
160
160
|
* 403 + code "insufficient_scope" (or an older server's
|
|
161
161
|
* WWW-Authenticate scope hint) → InsufficientScopeError
|
|
162
|
+
* 403 + code "acl_denied" → AclDeniedError (action, resource, reasons)
|
|
162
163
|
* 403 (other) → LoomcycleError (base)
|
|
163
164
|
* 404 + "snapshot" → SnapshotNotFoundError ────────┐
|
|
164
165
|
* 404 + "session" → SessionNotFoundError │ All extend
|
|
@@ -166,6 +167,7 @@ export async function deleteJSON(ctx, path, opts) {
|
|
|
166
167
|
* 404 + (other) → NotFoundError (base) │ catch any 404
|
|
167
168
|
* │ with one
|
|
168
169
|
* │ instanceof.
|
|
170
|
+
* 409 + an access-control code (group_external, *_exists) → LoomcycleError
|
|
169
171
|
* 409 + "already_pausing" / "already paused" → AlreadyPausingError
|
|
170
172
|
* 409 + "not_paused" / "not paused" → NotPausedError
|
|
171
173
|
* 409 + "session" → SessionBusyError
|
|
@@ -230,6 +232,15 @@ export async function raiseFromResponse(resp) {
|
|
|
230
232
|
requiredScope: stringField(bodyObject(bodyText)?.required_scope) ?? hinted,
|
|
231
233
|
});
|
|
232
234
|
}
|
|
235
|
+
if (opts.code === "acl_denied") {
|
|
236
|
+
const body = bodyObject(bodyText) ?? {};
|
|
237
|
+
throw new AclDeniedError(msg, {
|
|
238
|
+
...opts,
|
|
239
|
+
action: stringField(body.action),
|
|
240
|
+
resource: stringField(body.resource),
|
|
241
|
+
reasons: aclReasons(body.reasons),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
233
244
|
throw new LoomcycleError(msg, opts);
|
|
234
245
|
}
|
|
235
246
|
case 404:
|
|
@@ -248,6 +259,11 @@ export async function raiseFromResponse(resp) {
|
|
|
248
259
|
throw new AgentNotFoundError(msg, opts);
|
|
249
260
|
throw new NotFoundError(msg, opts);
|
|
250
261
|
case 409:
|
|
262
|
+
// The access-control conflicts carry a code, and their messages quote
|
|
263
|
+
// caller-chosen names (a group called "session-ops"), which the
|
|
264
|
+
// keyword matches below would misread.
|
|
265
|
+
if (opts.code !== undefined && ACL_CONFLICT_CODES.has(opts.code))
|
|
266
|
+
throw new LoomcycleError(msg, opts);
|
|
251
267
|
if (bodyLower.includes("already_pausing") || bodyLower.includes("already paused"))
|
|
252
268
|
throw new AlreadyPausingError(msg, opts);
|
|
253
269
|
if (bodyLower.includes("not_paused") || bodyLower.includes("not paused"))
|
|
@@ -339,6 +355,39 @@ export async function raiseFromResponse(resp) {
|
|
|
339
355
|
throw new LoomcycleError(msg, opts);
|
|
340
356
|
}
|
|
341
357
|
}
|
|
358
|
+
/** The 409 codes of the access-control management routes: a group an
|
|
359
|
+
* identity provider owns, and a row or membership that already exists. */
|
|
360
|
+
const ACL_CONFLICT_CODES = new Set([
|
|
361
|
+
"group_external",
|
|
362
|
+
"group_exists",
|
|
363
|
+
"group_member_exists",
|
|
364
|
+
"acl_exists",
|
|
365
|
+
"acl_binding_exists",
|
|
366
|
+
]);
|
|
367
|
+
/** aclReasons reads an `acl_denied` body's `reasons`, keeping only entries
|
|
368
|
+
* with the fields a reason always has: a proxy's or an older server's
|
|
369
|
+
* stray value must not surface as a rule. */
|
|
370
|
+
function aclReasons(v) {
|
|
371
|
+
if (!Array.isArray(v))
|
|
372
|
+
return [];
|
|
373
|
+
const out = [];
|
|
374
|
+
for (const r of v) {
|
|
375
|
+
if (r === null || typeof r !== "object")
|
|
376
|
+
continue;
|
|
377
|
+
const o = r;
|
|
378
|
+
const acl = stringField(o.acl);
|
|
379
|
+
const rule = numberField(o.rule);
|
|
380
|
+
const effect = o.effect;
|
|
381
|
+
if (acl === undefined || rule === undefined || (effect !== "allow" && effect !== "deny"))
|
|
382
|
+
continue;
|
|
383
|
+
const reason = { acl, rule, effect };
|
|
384
|
+
const version = numberField(o.version);
|
|
385
|
+
if (version !== undefined)
|
|
386
|
+
reason.version = version;
|
|
387
|
+
out.push(reason);
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
342
391
|
/** retryAfterMs reads a `Retry-After` header given in seconds, as the server
|
|
343
392
|
* always sends it, and returns milliseconds; undefined when the header is
|
|
344
393
|
* absent or not a number (an HTTP-date form is not parsed). */
|