@sdods/contracts 0.2.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.
@@ -0,0 +1,394 @@
1
+ import { z } from 'zod';
2
+ export const LayerSchema = z.enum(['ui', 'api', 'hybrid', 'recorded']);
3
+ export const BrowserSchema = z.enum([
4
+ 'chromium',
5
+ 'firefox',
6
+ 'webkit',
7
+ 'mobile-chrome',
8
+ 'mobile-safari',
9
+ ]);
10
+ export const ShotPolicySchema = z.enum(['off', 'on-failure', 'scenario', 'step', 'visual']);
11
+ export const SlugSchema = z
12
+ .string()
13
+ .regex(/^[a-z0-9][a-z0-9-]*$/, 'slug must be lowercase letters, digits and dashes');
14
+ export const DataSourceSchema = z.discriminatedUnion('type', [
15
+ z.object({ type: z.literal('csv'), path: z.string(), fallback: z.string().optional() }),
16
+ z.object({ type: z.literal('json'), path: z.string(), fallback: z.string().optional() }),
17
+ z.object({ type: z.literal('yaml'), path: z.string(), fallback: z.string().optional() }),
18
+ z.object({ type: z.literal('db'), table: z.string(), envColumn: z.string().default('env') }),
19
+ z.object({ type: z.literal('openapi'), spec: z.string(), schema: z.string() }),
20
+ ]);
21
+ export const UserPoolSchema = z.object({
22
+ dataset: z.string(),
23
+ roleColumn: z.string().default('role'),
24
+ leaseStore: z.enum(['file', 'db']).default('file'),
25
+ leaseTtlMs: z
26
+ .number()
27
+ .int()
28
+ .positive()
29
+ .default(10 * 60_000),
30
+ });
31
+ export const AuthStrategySchema = z.enum([
32
+ 'none',
33
+ 'form',
34
+ 'token',
35
+ 'oauth-client-credentials',
36
+ 'sso',
37
+ 'custom',
38
+ ]);
39
+ export const AuthFormSchema = z.object({
40
+ loginPath: z.string().default('/'),
41
+ usernameSelector: z.string(),
42
+ passwordSelector: z.string(),
43
+ submitSelector: z.string(),
44
+ readySelector: z.string().optional(),
45
+ readyUrl: z.string().optional(),
46
+ });
47
+ export const ProjectAuthSchema = z.object({
48
+ strategy: AuthStrategySchema.default('none'),
49
+ storageState: z.boolean().default(true),
50
+ maxAgeMinutes: z.number().int().positive().default(60),
51
+ form: AuthFormSchema.optional(),
52
+ tokenPlacement: z
53
+ .object({
54
+ kind: z.enum(['localStorage', 'cookie', 'header']).default('header'),
55
+ name: z.string().default('Authorization'),
56
+ prefix: z.string().default('Bearer '),
57
+ })
58
+ .optional(),
59
+ });
60
+ export const ScreenshotConfigSchema = z.object({
61
+ policy: z.record(z.string(), ShotPolicySchema).default({ default: 'on-failure' }),
62
+ fullPage: z.boolean().default(false),
63
+ mask: z.array(z.string()).default([]),
64
+ viewport: z
65
+ .object({ width: z.number().int().positive(), height: z.number().int().positive() })
66
+ .default({ width: 1280, height: 720 }),
67
+ onlyOnFailure: z.boolean().default(false),
68
+ });
69
+ export const HealConfigSchema = z.object({
70
+ enabled: z.boolean().default(true),
71
+ primaryTimeoutMs: z.number().int().positive().default(3000),
72
+ probeTimeoutMs: z.number().int().positive().default(1500),
73
+ minScore: z.number().min(0).max(1).default(0.6),
74
+ actions: z
75
+ .array(z.enum(['click', 'fill', 'select', 'assert', 'hover', 'check']))
76
+ .default(['click', 'fill', 'select', 'assert', 'hover', 'check']),
77
+ });
78
+ export const TimeoutsSchema = z.object({
79
+ test: z.number().int().positive().default(60_000),
80
+ expect: z.number().int().positive().default(10_000),
81
+ action: z.number().int().positive().default(15_000),
82
+ navigation: z.number().int().positive().default(30_000),
83
+ api: z.number().int().positive().default(15_000),
84
+ });
85
+ export const RetriesSchema = z.object({
86
+ ci: z.number().int().min(0).default(2),
87
+ local: z.number().int().min(0).default(0),
88
+ byTag: z.record(z.string(), z.number().int().min(0)).default({}),
89
+ });
90
+ export const PerfBudgetsSchema = z.object({
91
+ pageLoadMs: z.number().positive().optional(),
92
+ lcpMs: z.number().positive().optional(),
93
+ fcpMs: z.number().positive().optional(),
94
+ ttfbMs: z.number().positive().optional(),
95
+ apiP95Ms: z.number().positive().optional(),
96
+ });
97
+ export const GitHubIntegrationSchema = z.object({
98
+ enabled: z.boolean().default(false),
99
+ owner: z.string().optional(),
100
+ repo: z.string().optional(),
101
+ checkRun: z.boolean().default(true),
102
+ prComment: z.boolean().default(true),
103
+ createIssueOnFailure: z.enum(['never', 'smoke', 'always']).default('never'),
104
+ closeOnPass: z.boolean().default(false),
105
+ labels: z.array(z.string()).default(['sdods']),
106
+ tokenEnv: z.string().default('GITHUB_TOKEN'),
107
+ uploadToRelease: z.string().optional(),
108
+ });
109
+ export const JiraIntegrationSchema = z.object({
110
+ enabled: z.boolean().default(false),
111
+ baseUrl: z.string().url().optional(),
112
+ projectKey: z.string().optional(),
113
+ issueType: z.string().default('Bug'),
114
+ authMode: z.enum(['basic', 'bearer']).default('basic'),
115
+ emailEnv: z.string().default('JIRA_EMAIL'),
116
+ tokenEnv: z.string().default('JIRA_API_TOKEN'),
117
+ createIssueOnFailure: z.enum(['never', 'smoke', 'always']).default('never'),
118
+ transitionOnPass: z.string().optional(),
119
+ linkTaggedScenarios: z.boolean().default(true),
120
+ labels: z.array(z.string()).default(['sdods']),
121
+ maxAttachmentMb: z.number().positive().default(10),
122
+ });
123
+ export const IntegrationsSchema = z.object({
124
+ github: GitHubIntegrationSchema.optional(),
125
+ jira: JiraIntegrationSchema.optional(),
126
+ custom: z
127
+ .array(z.object({ module: z.string(), options: z.record(z.string(), z.unknown()).optional() }))
128
+ .default([]),
129
+ });
130
+ const ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
131
+ export const McpServerSchema = z.discriminatedUnion('transport', [
132
+ z.object({
133
+ transport: z.literal('stdio'),
134
+ command: z.string(),
135
+ args: z.array(z.string()).default([]),
136
+ envFrom: z
137
+ .record(z.string(), z.string().regex(ENV_NAME, 'must be an env var NAME'))
138
+ .default({}),
139
+ allowedTools: z.array(z.string()).optional(),
140
+ enabled: z.boolean().default(true),
141
+ timeoutMs: z.number().int().positive().default(60_000),
142
+ }),
143
+ z.object({
144
+ transport: z.literal('http'),
145
+ url: z.string().url(),
146
+ headersFrom: z
147
+ .record(z.string(), z.string().regex(ENV_NAME, 'must be an env var NAME'))
148
+ .default({}),
149
+ allowedTools: z.array(z.string()).optional(),
150
+ enabled: z.boolean().default(true),
151
+ timeoutMs: z.number().int().positive().default(60_000),
152
+ }),
153
+ ]);
154
+ export const McpConfigSchema = z.object({
155
+ servers: z.record(z.string(), McpServerSchema).default({}),
156
+ });
157
+ /** Settings for a model server you run yourself: Ollama, vLLM, LM Studio, llama.cpp. */
158
+ export const LocalLlmConfigSchema = z
159
+ .object({
160
+ /** Ollama's native endpoint, or the OpenAI-compatible base URL of another server. */
161
+ baseUrl: z.string().url().optional(),
162
+ /**
163
+ * `options.num_ctx`. Ollama loads a model at 4096 tokens whatever its weights allow, and
164
+ * truncates a longer prompt without saying so, so this is the setting that decides whether
165
+ * an agent job sees its whole prompt.
166
+ */
167
+ contextTokens: z.number().int().min(2048).max(1_048_576).optional(),
168
+ temperature: z.number().min(0).max(2).optional(),
169
+ /** Local generation is minutes, not seconds. */
170
+ requestTimeoutMs: z.number().int().positive().default(300_000),
171
+ /** How long the server keeps the model in memory between turns. */
172
+ keepAlive: z.string().default('10m'),
173
+ })
174
+ .default({ requestTimeoutMs: 300_000, keepAlive: '10m' });
175
+ export const AgentsConfigSchema = z.object({
176
+ /**
177
+ * claude/openai-compatible use API keys; claude-code/codex shell out to the logged-in CLI;
178
+ * ollama runs a model on this machine, with no key and no per-token cost.
179
+ */
180
+ provider: z
181
+ .enum(['claude', 'claude-code', 'codex', 'openai-compatible', 'ollama', 'fake'])
182
+ .optional(),
183
+ /**
184
+ * How much of the platform a model is shown. `auto` picks `small` for a local or small model:
185
+ * a handful of tools, one call per turn, and the project's real step patterns in the prompt.
186
+ */
187
+ profile: z.enum(['auto', 'full', 'small']).default('auto'),
188
+ models: z.record(z.string(), z.string()).default({}),
189
+ maxTurns: z.record(z.string(), z.number().int().positive()).default({}),
190
+ budgetUsd: z.record(z.string(), z.number().positive()).default({ default: 2 }),
191
+ maxRunsPerJob: z.number().int().positive().default(3),
192
+ sourceRoots: z.array(z.string()).default([]),
193
+ local: LocalLlmConfigSchema,
194
+ });
195
+ export const ScheduleSchema = z.object({
196
+ name: z.string(),
197
+ cron: z.string(),
198
+ timezone: z.string().default('UTC'),
199
+ env: z.string().optional(),
200
+ tags: z.string().optional(),
201
+ layers: z.array(LayerSchema).optional(),
202
+ browsers: z.array(BrowserSchema).optional(),
203
+ workers: z.number().int().positive().optional(),
204
+ harMode: z.enum(['off', 'update', 'replay']).optional(),
205
+ overlap: z.enum(['skip', 'queue', 'cancel-previous']).default('skip'),
206
+ jitterSeconds: z.number().int().min(0).default(0),
207
+ catchUp: z.boolean().default(false),
208
+ enabled: z.boolean().default(true),
209
+ notify: z.array(z.enum(['github', 'jira', 'webhook'])).default([]),
210
+ retentionRuns: z.number().int().positive().optional(),
211
+ });
212
+ /**
213
+ * Hierarchy: organization → workspace → project → module.
214
+ * A module is a feature area of an application (auth, inventory, checkout …) that owns a
215
+ * features directory, default tags and an owner. A process is a named, repeatable run recipe
216
+ * (pr-check, nightly-regression, release-gate …) that pins tags/layers/browsers/env and a trigger.
217
+ */
218
+ export const OrganizationSchema = z.object({
219
+ slug: SlugSchema,
220
+ name: z.string().min(1),
221
+ description: z.string().optional(),
222
+ url: z.string().url().optional(),
223
+ });
224
+ export const WorkspaceSchema = z.object({
225
+ slug: SlugSchema,
226
+ name: z.string().min(1),
227
+ description: z.string().optional(),
228
+ organization: SlugSchema.optional(),
229
+ });
230
+ export const TestingTypeSchema = z.enum([
231
+ 'functional',
232
+ 'smoke',
233
+ 'regression',
234
+ 'sanity',
235
+ 'integration',
236
+ 'contract',
237
+ 'visual',
238
+ 'accessibility',
239
+ 'performance',
240
+ 'security',
241
+ 'data-driven',
242
+ 'exploratory',
243
+ ]);
244
+ export const ModuleSchema = z.object({
245
+ name: SlugSchema,
246
+ title: z.string().optional(),
247
+ description: z.string().optional(),
248
+ /** directory under features/ (default: the module name) */
249
+ path: z.string().optional(),
250
+ layers: z.array(LayerSchema).optional(),
251
+ testingTypes: z.array(TestingTypeSchema).default(['functional']),
252
+ /** tags every scenario in this module is expected to carry (lint warns when missing) */
253
+ tags: z.array(z.string()).default([]),
254
+ owner: z.string().optional(),
255
+ jiraComponent: z.string().optional(),
256
+ routes: z.array(z.string()).default([]),
257
+ endpoints: z.array(z.string()).default([]),
258
+ });
259
+ export const ProcessTriggerSchema = z.enum([
260
+ 'manual',
261
+ 'pr',
262
+ 'merge',
263
+ 'nightly',
264
+ 'release',
265
+ 'schedule',
266
+ 'webhook',
267
+ ]);
268
+ export const ProcessSchema = z.object({
269
+ name: SlugSchema,
270
+ title: z.string().optional(),
271
+ description: z.string().optional(),
272
+ trigger: ProcessTriggerSchema.default('manual'),
273
+ env: z.string().optional(),
274
+ tags: z.string().optional(),
275
+ layers: z.array(LayerSchema).optional(),
276
+ browsers: z.array(BrowserSchema).optional(),
277
+ modules: z.array(SlugSchema).optional(),
278
+ workers: z.number().int().positive().optional(),
279
+ retries: z.number().int().min(0).optional(),
280
+ harMode: z.enum(['off', 'update', 'replay']).optional(),
281
+ failOnFlaky: z.boolean().default(false),
282
+ gates: z
283
+ .object({
284
+ minPassRate: z.number().min(0).max(100).optional(),
285
+ maxFlaky: z.number().int().min(0).optional(),
286
+ perfBudgets: z.boolean().default(false),
287
+ a11y: z.boolean().default(false),
288
+ })
289
+ .default({ perfBudgets: false, a11y: false }),
290
+ notify: z.array(z.enum(['github', 'jira', 'webhook'])).default([]),
291
+ schedule: z.string().optional(),
292
+ });
293
+ export const ProjectConfigSchema = z.object({
294
+ slug: SlugSchema,
295
+ name: z.string().min(1),
296
+ description: z.string().optional(),
297
+ organization: SlugSchema.optional(),
298
+ workspace: SlugSchema.optional(),
299
+ modules: z.array(ModuleSchema).default([]),
300
+ processes: z.array(ProcessSchema).default([]),
301
+ layers: z.array(LayerSchema).min(1),
302
+ browsers: z.array(BrowserSchema).min(1).default(['chromium']),
303
+ channel: z.enum(['chrome', 'msedge', 'chrome-beta', 'msedge-beta']).optional(),
304
+ testIdAttribute: z.string().default('data-testid'),
305
+ routes: z.record(z.string(), z.string()).default({}),
306
+ tags: z
307
+ .object({
308
+ suites: z.array(z.string()).min(1).default(['smoke', 'regression', 'sanity']),
309
+ extra: z.array(z.string()).default([]),
310
+ roles: z.array(z.string()).default([]),
311
+ })
312
+ .default({ suites: ['smoke', 'regression', 'sanity'], extra: [], roles: [] }),
313
+ envs: z.object({ default: z.string(), available: z.array(z.string()).min(1) }),
314
+ data: z
315
+ .object({
316
+ sources: z.record(z.string(), DataSourceSchema).default({}),
317
+ factories: z.string().optional(),
318
+ userPool: UserPoolSchema.optional(),
319
+ })
320
+ .default({ sources: {} }),
321
+ auth: ProjectAuthSchema.default({ strategy: 'none', storageState: true, maxAgeMinutes: 60 }),
322
+ screenshots: ScreenshotConfigSchema.default({
323
+ policy: { default: 'on-failure' },
324
+ fullPage: false,
325
+ mask: [],
326
+ viewport: { width: 1280, height: 720 },
327
+ onlyOnFailure: false,
328
+ }),
329
+ heal: HealConfigSchema.default({
330
+ enabled: true,
331
+ primaryTimeoutMs: 3000,
332
+ probeTimeoutMs: 1500,
333
+ minScore: 0.6,
334
+ actions: ['click', 'fill', 'select', 'assert', 'hover', 'check'],
335
+ }),
336
+ timeouts: TimeoutsSchema.default({
337
+ test: 60_000,
338
+ expect: 10_000,
339
+ action: 15_000,
340
+ navigation: 30_000,
341
+ api: 15_000,
342
+ }),
343
+ retries: RetriesSchema.default({ ci: 2, local: 0, byTag: {} }),
344
+ perf: z.object({ budgets: PerfBudgetsSchema.default({}) }).default({ budgets: {} }),
345
+ integrations: IntegrationsSchema.default({ custom: [] }),
346
+ mcp: McpConfigSchema.default({ servers: {} }),
347
+ agents: AgentsConfigSchema.default({
348
+ models: {},
349
+ maxTurns: {},
350
+ profile: 'auto',
351
+ budgetUsd: { default: 2 },
352
+ maxRunsPerJob: 3,
353
+ sourceRoots: [],
354
+ local: { requestTimeoutMs: 300_000, keepAlive: '10m' },
355
+ }),
356
+ schedules: z.array(ScheduleSchema).default([]),
357
+ reports: z
358
+ .object({
359
+ cucumberHtml: z.boolean().default(false),
360
+ allure: z.boolean().default(false),
361
+ junit: z.boolean().default(false),
362
+ })
363
+ .default({ cucumberHtml: false, allure: false, junit: false }),
364
+ /** How the CI matrix (`sdods project list --matrix`) treats this project. */
365
+ ci: z
366
+ .object({
367
+ /** false = excluded from the generic browser matrix (e.g. a project that needs its own server) */
368
+ enabled: z.boolean().default(true),
369
+ /** environment the matrix runs against (default: envs.default) */
370
+ env: z.string().optional(),
371
+ /** tag expression for the matrix run (default: the workflow's choice) */
372
+ tags: z.string().optional(),
373
+ /** browsers to include in the matrix (default: project browsers) */
374
+ browsers: z.array(BrowserSchema).optional(),
375
+ })
376
+ .default({ enabled: true }),
377
+ });
378
+ /** Root `sdods.workspace.yaml`: names the organization and workspace every project in the repo belongs to. */
379
+ export const WorkspaceFileSchema = z.object({
380
+ organization: OrganizationSchema,
381
+ /** one organization can hold many workspaces; projects pick one via `workspace:` */
382
+ workspaces: z.array(WorkspaceSchema).min(1),
383
+ /** workspace used by projects that do not set `workspace:` */
384
+ defaultWorkspace: SlugSchema,
385
+ defaults: z
386
+ .object({
387
+ browsers: z.array(BrowserSchema).optional(),
388
+ suites: z.array(z.string()).optional(),
389
+ testIdAttribute: z.string().optional(),
390
+ processes: z.array(ProcessSchema).default([]),
391
+ })
392
+ .default({ processes: [] }),
393
+ });
394
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1,32 @@
1
+ export declare const SCOPES: readonly ["orgs:read", "orgs:admin", "workspaces:read", "workspaces:write", "projects:read", "projects:write", "envs:read", "envs:write", "datasets:read", "datasets:write", "runs:read", "runs:write", "runs:ingest", "artifacts:read", "features:read", "features:write", "agents:run", "agents:review", "integrations:read", "integrations:write", "schedules:read", "schedules:write", "processes:read", "processes:write", "users:admin", "audit:read"];
2
+ export type Scope = (typeof SCOPES)[number];
3
+ /** Platform-wide roles (a user's baseline). */
4
+ export declare const ROLES: readonly ["viewer", "editor", "admin"];
5
+ export type Role = (typeof ROLES)[number];
6
+ /** Organization membership roles. */
7
+ export declare const ORG_ROLES: readonly ["member", "admin", "owner"];
8
+ export type OrgRole = (typeof ORG_ROLES)[number];
9
+ /** Workspace membership roles. */
10
+ export declare const WORKSPACE_ROLES: readonly ["viewer", "editor", "admin"];
11
+ export type WorkspaceRole = (typeof WORKSPACE_ROLES)[number];
12
+ export declare const ROLE_SCOPES: Record<Role, readonly Scope[]>;
13
+ /** Org role → implied workspace role for every workspace in that org. */
14
+ export declare const ORG_ROLE_TO_WORKSPACE_ROLE: Record<OrgRole, WorkspaceRole>;
15
+ /**
16
+ * Effective role inside a workspace = the higher of the explicit workspace membership and the
17
+ * role implied by the organization membership. Platform admins are admin everywhere.
18
+ */
19
+ export declare function effectiveWorkspaceRole(input: {
20
+ platformRole?: Role;
21
+ orgRole?: OrgRole;
22
+ workspaceRole?: WorkspaceRole;
23
+ }): WorkspaceRole | null;
24
+ export declare function scopesForRole(role: Role): Scope[];
25
+ export declare function scopesForWorkspaceRole(role: WorkspaceRole): Scope[];
26
+ export declare function isScope(v: string): v is Scope;
27
+ export declare function hasScope(granted: readonly string[], required: Scope): boolean;
28
+ /** Tool access levels map onto scopes so the MCP registry can gate uniformly. */
29
+ export type ToolAccess = 'read' | 'run' | 'write';
30
+ export declare function scopeForToolAccess(access: ToolAccess, domain: string): Scope | null;
31
+ export declare const API_TOKEN_PREFIX = "amx_";
32
+ //# sourceMappingURL=scopes.d.ts.map
package/dist/scopes.js ADDED
@@ -0,0 +1,95 @@
1
+ export const SCOPES = [
2
+ 'orgs:read',
3
+ 'orgs:admin',
4
+ 'workspaces:read',
5
+ 'workspaces:write',
6
+ 'projects:read',
7
+ 'projects:write',
8
+ 'envs:read',
9
+ 'envs:write',
10
+ 'datasets:read',
11
+ 'datasets:write',
12
+ 'runs:read',
13
+ 'runs:write',
14
+ 'runs:ingest',
15
+ 'artifacts:read',
16
+ 'features:read',
17
+ 'features:write',
18
+ 'agents:run',
19
+ 'agents:review',
20
+ 'integrations:read',
21
+ 'integrations:write',
22
+ 'schedules:read',
23
+ 'schedules:write',
24
+ 'processes:read',
25
+ 'processes:write',
26
+ 'users:admin',
27
+ 'audit:read',
28
+ ];
29
+ /** Platform-wide roles (a user's baseline). */
30
+ export const ROLES = ['viewer', 'editor', 'admin'];
31
+ /** Organization membership roles. */
32
+ export const ORG_ROLES = ['member', 'admin', 'owner'];
33
+ /** Workspace membership roles. */
34
+ export const WORKSPACE_ROLES = ['viewer', 'editor', 'admin'];
35
+ const READ_SCOPES = SCOPES.filter((s) => s.endsWith(':read'));
36
+ export const ROLE_SCOPES = {
37
+ viewer: [...READ_SCOPES, 'artifacts:read'],
38
+ editor: [
39
+ ...READ_SCOPES,
40
+ 'artifacts:read',
41
+ 'runs:write',
42
+ 'runs:ingest',
43
+ 'features:write',
44
+ 'datasets:write',
45
+ 'envs:write',
46
+ 'schedules:write',
47
+ 'processes:write',
48
+ 'agents:run',
49
+ 'agents:review',
50
+ ],
51
+ admin: [...SCOPES],
52
+ };
53
+ /** Org role → implied workspace role for every workspace in that org. */
54
+ export const ORG_ROLE_TO_WORKSPACE_ROLE = {
55
+ member: 'viewer',
56
+ admin: 'admin',
57
+ owner: 'admin',
58
+ };
59
+ const ROLE_RANK = { viewer: 0, editor: 1, admin: 2 };
60
+ /**
61
+ * Effective role inside a workspace = the higher of the explicit workspace membership and the
62
+ * role implied by the organization membership. Platform admins are admin everywhere.
63
+ */
64
+ export function effectiveWorkspaceRole(input) {
65
+ if (input.platformRole === 'admin')
66
+ return 'admin';
67
+ const candidates = [];
68
+ if (input.workspaceRole)
69
+ candidates.push(input.workspaceRole);
70
+ if (input.orgRole)
71
+ candidates.push(ORG_ROLE_TO_WORKSPACE_ROLE[input.orgRole]);
72
+ if (candidates.length === 0)
73
+ return null;
74
+ return candidates.sort((a, b) => ROLE_RANK[b] - ROLE_RANK[a])[0];
75
+ }
76
+ export function scopesForRole(role) {
77
+ return [...new Set(ROLE_SCOPES[role])];
78
+ }
79
+ export function scopesForWorkspaceRole(role) {
80
+ // workspace roles map 1:1 onto the platform role scope sets, minus platform-only scopes
81
+ const base = scopesForRole(role);
82
+ return base.filter((s) => s !== 'users:admin' && s !== 'orgs:admin');
83
+ }
84
+ export function isScope(v) {
85
+ return SCOPES.includes(v);
86
+ }
87
+ export function hasScope(granted, required) {
88
+ return granted.includes(required);
89
+ }
90
+ export function scopeForToolAccess(access, domain) {
91
+ const candidate = `${domain}:${access === 'read' ? 'read' : 'write'}`;
92
+ return isScope(candidate) ? candidate : null;
93
+ }
94
+ export const API_TOKEN_PREFIX = 'amx_';
95
+ //# sourceMappingURL=scopes.js.map