ai-database 2.0.2 → 2.1.1

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/actions.d.ts +247 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +260 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/ai-promise-db.d.ts +34 -2
  7. package/dist/ai-promise-db.d.ts.map +1 -1
  8. package/dist/ai-promise-db.js +511 -66
  9. package/dist/ai-promise-db.js.map +1 -1
  10. package/dist/constants.d.ts +16 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +16 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/events.d.ts +153 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +154 -0
  17. package/dist/events.js.map +1 -0
  18. package/dist/index.d.ts +8 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +13 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/memory-provider.d.ts +144 -2
  23. package/dist/memory-provider.d.ts.map +1 -1
  24. package/dist/memory-provider.js +569 -13
  25. package/dist/memory-provider.js.map +1 -1
  26. package/dist/schema/cascade.d.ts +96 -0
  27. package/dist/schema/cascade.d.ts.map +1 -0
  28. package/dist/schema/cascade.js +528 -0
  29. package/dist/schema/cascade.js.map +1 -0
  30. package/dist/schema/index.d.ts +197 -0
  31. package/dist/schema/index.d.ts.map +1 -0
  32. package/dist/schema/index.js +1211 -0
  33. package/dist/schema/index.js.map +1 -0
  34. package/dist/schema/parse.d.ts +225 -0
  35. package/dist/schema/parse.d.ts.map +1 -0
  36. package/dist/schema/parse.js +732 -0
  37. package/dist/schema/parse.js.map +1 -0
  38. package/dist/schema/provider.d.ts +176 -0
  39. package/dist/schema/provider.d.ts.map +1 -0
  40. package/dist/schema/provider.js +258 -0
  41. package/dist/schema/provider.js.map +1 -0
  42. package/dist/schema/resolve.d.ts +87 -0
  43. package/dist/schema/resolve.d.ts.map +1 -0
  44. package/dist/schema/resolve.js +474 -0
  45. package/dist/schema/resolve.js.map +1 -0
  46. package/dist/schema/semantic.d.ts +53 -0
  47. package/dist/schema/semantic.d.ts.map +1 -0
  48. package/dist/schema/semantic.js +247 -0
  49. package/dist/schema/semantic.js.map +1 -0
  50. package/dist/schema/types.d.ts +528 -0
  51. package/dist/schema/types.d.ts.map +1 -0
  52. package/dist/schema/types.js +9 -0
  53. package/dist/schema/types.js.map +1 -0
  54. package/dist/schema.d.ts +24 -867
  55. package/dist/schema.d.ts.map +1 -1
  56. package/dist/schema.js +41 -1124
  57. package/dist/schema.js.map +1 -1
  58. package/dist/semantic.d.ts +175 -0
  59. package/dist/semantic.d.ts.map +1 -0
  60. package/dist/semantic.js +338 -0
  61. package/dist/semantic.js.map +1 -0
  62. package/dist/types.d.ts +14 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/package.json +13 -4
  66. package/.turbo/turbo-build.log +0 -5
  67. package/TESTING.md +0 -410
  68. package/TEST_SUMMARY.md +0 -250
  69. package/TODO.md +0 -128
  70. package/src/ai-promise-db.ts +0 -1243
  71. package/src/authorization.ts +0 -1102
  72. package/src/durable-clickhouse.ts +0 -596
  73. package/src/durable-promise.ts +0 -582
  74. package/src/execution-queue.ts +0 -608
  75. package/src/index.test.ts +0 -868
  76. package/src/index.ts +0 -337
  77. package/src/linguistic.ts +0 -404
  78. package/src/memory-provider.test.ts +0 -1036
  79. package/src/memory-provider.ts +0 -1119
  80. package/src/schema.test.ts +0 -1254
  81. package/src/schema.ts +0 -2296
  82. package/src/tests.ts +0 -725
  83. package/src/types.ts +0 -1177
  84. package/test/README.md +0 -153
  85. package/test/edge-cases.test.ts +0 -646
  86. package/test/provider-resolution.test.ts +0 -402
  87. package/tsconfig.json +0 -9
  88. package/vitest.config.ts +0 -19
@@ -1,1102 +0,0 @@
1
- /**
2
- * Authorization Primitives (FGA/RBAC)
3
- *
4
- * Based on WorkOS FGA design - Fine-Grained Authorization that:
5
- * - Extends RBAC with resource-scoped, hierarchical permissions
6
- * - Uses Subjects, Resources, Roles, Permissions, Assignments
7
- * - Supports automatic inheritance through resource hierarchy
8
- * - Integrates with Noun/Verb for action authorization
9
- *
10
- * @see https://workos.com/docs/fga
11
- * @packageDocumentation
12
- */
13
-
14
- import type { Noun, Verb } from './schema.js'
15
-
16
- // =============================================================================
17
- // Core FGA Primitives
18
- // =============================================================================
19
-
20
- /**
21
- * Subject - who is requesting access
22
- *
23
- * Can be a user, group, service account, AI agent, or any entity
24
- * that can be granted permissions.
25
- */
26
- export interface Subject {
27
- /** Subject type (user, group, service, agent) */
28
- type: string
29
-
30
- /** Unique identifier within the type */
31
- id: string
32
-
33
- /** Optional display name */
34
- name?: string
35
-
36
- /** Subject metadata */
37
- metadata?: Record<string, unknown>
38
- }
39
-
40
- /**
41
- * Subject types
42
- */
43
- export type SubjectType =
44
- | 'user' // Human user
45
- | 'group' // Group of users
46
- | 'team' // Team (organizational unit)
47
- | 'service' // Service account
48
- | 'agent' // AI agent
49
- | 'role' // Role (for role inheritance)
50
- | string // Custom subject types
51
-
52
- /**
53
- * Resource - what is being accessed
54
- *
55
- * Any entity in the system that can have permissions.
56
- * Resources form a hierarchy (up to 5 levels).
57
- */
58
- export interface Resource {
59
- /** Resource type (maps to Noun) */
60
- type: string
61
-
62
- /** Unique identifier */
63
- id: string
64
-
65
- /** Parent resource (for hierarchy) */
66
- parent?: ResourceRef
67
-
68
- /** Resource metadata */
69
- metadata?: Record<string, unknown>
70
- }
71
-
72
- /**
73
- * Resource reference (lightweight)
74
- */
75
- export interface ResourceRef {
76
- type: string
77
- id: string
78
- }
79
-
80
- /**
81
- * Resource type definition
82
- */
83
- export interface ResourceType {
84
- /** Type name (should match a Noun) */
85
- name: string
86
-
87
- /** Human-readable description */
88
- description?: string
89
-
90
- /** Parent type in hierarchy (null = root) */
91
- parentType?: string
92
-
93
- /** Actions available on this resource type */
94
- actions?: string[]
95
-
96
- /** Whether this is a high-cardinality type (kept local, not synced) */
97
- local?: boolean
98
- }
99
-
100
- // =============================================================================
101
- // Role & Permission
102
- // =============================================================================
103
-
104
- /**
105
- * Permission - what action can be performed on what resource type
106
- */
107
- export interface Permission {
108
- /** Permission name (e.g., 'read', 'write', 'admin') */
109
- name: string
110
-
111
- /** Description */
112
- description?: string
113
-
114
- /** Resource type this permission applies to */
115
- resourceType: string
116
-
117
- /** Actions granted (maps to Verb actions) */
118
- actions: string[]
119
-
120
- /** Whether this permission extends to child resource types */
121
- inheritable?: boolean
122
- }
123
-
124
- /**
125
- * Role - a named collection of permissions
126
- *
127
- * Roles are scoped to resource types. A "Workspace Admin" role
128
- * only makes sense on Workspace resources.
129
- */
130
- export interface Role {
131
- /** Role identifier */
132
- id: string
133
-
134
- /** Display name */
135
- name: string
136
-
137
- /** Description */
138
- description?: string
139
-
140
- /** Resource type this role is scoped to */
141
- resourceType: string
142
-
143
- /** Permissions included in this role */
144
- permissions: Permission[]
145
-
146
- /** Parent roles (for role inheritance) */
147
- inherits?: string[]
148
-
149
- /** Metadata */
150
- metadata?: Record<string, unknown>
151
- }
152
-
153
- /**
154
- * Standard role levels (can be customized)
155
- */
156
- export type RoleLevel =
157
- | 'owner' // Full control, can delete
158
- | 'admin' // Full control, cannot delete
159
- | 'editor' // Can modify
160
- | 'viewer' // Read-only
161
- | 'guest' // Limited read
162
- | string // Custom levels
163
-
164
- // =============================================================================
165
- // Assignment (Warrant)
166
- // =============================================================================
167
-
168
- /**
169
- * Assignment - binds a subject to a role on a resource
170
- *
171
- * This is the core authorization tuple: (subject, role, resource)
172
- * Also called a "warrant" in some FGA systems.
173
- */
174
- export interface Assignment {
175
- /** Unique assignment ID */
176
- id: string
177
-
178
- /** Who is granted access */
179
- subject: Subject
180
-
181
- /** What role is granted */
182
- role: string
183
-
184
- /** On what resource */
185
- resource: ResourceRef
186
-
187
- /** When the assignment was created */
188
- createdAt: Date
189
-
190
- /** Who created the assignment */
191
- createdBy?: Subject
192
-
193
- /** Optional expiration */
194
- expiresAt?: Date
195
-
196
- /** Assignment metadata */
197
- metadata?: Record<string, unknown>
198
- }
199
-
200
- /**
201
- * Assignment input (for creating assignments)
202
- */
203
- export interface AssignmentInput {
204
- subject: Subject | string // Can use "user:123" format
205
- role: string
206
- resource: ResourceRef | string // Can use "workspace:456" format
207
- expiresAt?: Date
208
- metadata?: Record<string, unknown>
209
- }
210
-
211
- // =============================================================================
212
- // Authorization Check
213
- // =============================================================================
214
-
215
- /**
216
- * Authorization check request
217
- */
218
- export interface AuthzCheckRequest {
219
- /** Who is requesting */
220
- subject: Subject | string
221
-
222
- /** What action */
223
- action: string
224
-
225
- /** On what resource */
226
- resource: ResourceRef | string
227
-
228
- /** Additional context */
229
- context?: Record<string, unknown>
230
- }
231
-
232
- /**
233
- * Authorization check result
234
- */
235
- export interface AuthzCheckResult {
236
- /** Whether access is allowed */
237
- allowed: boolean
238
-
239
- /** Why (for debugging/audit) */
240
- reason?: string
241
-
242
- /** Which assignment granted access (if allowed) */
243
- assignment?: Assignment
244
-
245
- /** Check latency in ms */
246
- latencyMs?: number
247
- }
248
-
249
- /**
250
- * Batch authorization check
251
- */
252
- export interface AuthzBatchCheckRequest {
253
- checks: AuthzCheckRequest[]
254
- }
255
-
256
- export interface AuthzBatchCheckResult {
257
- results: AuthzCheckResult[]
258
- latencyMs: number
259
- }
260
-
261
- // =============================================================================
262
- // Resource Hierarchy
263
- // =============================================================================
264
-
265
- /**
266
- * Resource hierarchy definition
267
- *
268
- * Defines the parent-child relationships between resource types.
269
- * Permissions can flow down through the hierarchy.
270
- *
271
- * @example
272
- * ```ts
273
- * const hierarchy: ResourceHierarchy = {
274
- * levels: [
275
- * { type: 'organization', depth: 0 },
276
- * { type: 'workspace', depth: 1, parentType: 'organization' },
277
- * { type: 'project', depth: 2, parentType: 'workspace' },
278
- * { type: 'document', depth: 3, parentType: 'project' },
279
- * ],
280
- * maxDepth: 5,
281
- * }
282
- * ```
283
- */
284
- export interface ResourceHierarchy {
285
- /** Hierarchy levels */
286
- levels: ResourceType[]
287
-
288
- /** Maximum depth (WorkOS supports up to 5) */
289
- maxDepth: number
290
- }
291
-
292
- /**
293
- * Common SaaS resource hierarchies
294
- */
295
- export const StandardHierarchies = {
296
- /** Organization → Workspace → Project → Resource */
297
- saas: {
298
- levels: [
299
- { name: 'organization', description: 'Top-level organization' },
300
- { name: 'workspace', description: 'Workspace within org', parentType: 'organization' },
301
- { name: 'project', description: 'Project within workspace', parentType: 'workspace' },
302
- { name: 'resource', description: 'Resource within project', parentType: 'project' },
303
- ],
304
- maxDepth: 4,
305
- },
306
-
307
- /** Organization → Team → Repository */
308
- devtools: {
309
- levels: [
310
- { name: 'organization', description: 'Top-level organization' },
311
- { name: 'team', description: 'Team within org', parentType: 'organization' },
312
- { name: 'repository', description: 'Repository owned by team', parentType: 'team' },
313
- ],
314
- maxDepth: 3,
315
- },
316
-
317
- /** Account → Folder → Document */
318
- documents: {
319
- levels: [
320
- { name: 'account', description: 'User account' },
321
- { name: 'folder', description: 'Folder in account', parentType: 'account' },
322
- { name: 'document', description: 'Document in folder', parentType: 'folder' },
323
- ],
324
- maxDepth: 3,
325
- },
326
- } as const satisfies Record<string, ResourceHierarchy>
327
-
328
- // =============================================================================
329
- // Role Definitions
330
- // =============================================================================
331
-
332
- /**
333
- * Standard permissions
334
- *
335
- * Beyond CRUD, we add `act` for domain-specific verbs (send, pay, publish, etc.)
336
- *
337
- * Permission levels:
338
- * - read → view data (GET operations)
339
- * - edit → modify data (PUT/PATCH operations)
340
- * - act → perform actions/verbs (POST operations with side effects)
341
- * - delete → remove (DELETE operations)
342
- * - manage → all + role assignment
343
- */
344
- export const StandardPermissions = {
345
- create: (resourceType: string): Permission => ({
346
- name: 'create',
347
- description: `Create ${resourceType}`,
348
- resourceType,
349
- actions: ['create'],
350
- inheritable: true,
351
- }),
352
-
353
- read: (resourceType: string): Permission => ({
354
- name: 'read',
355
- description: `Read ${resourceType}`,
356
- resourceType,
357
- actions: ['read', 'get', 'list', 'search', 'view'],
358
- inheritable: true,
359
- }),
360
-
361
- edit: (resourceType: string): Permission => ({
362
- name: 'edit',
363
- description: `Edit ${resourceType}`,
364
- resourceType,
365
- actions: ['update', 'edit', 'modify', 'patch'],
366
- inheritable: true,
367
- }),
368
-
369
- /**
370
- * Act - perform domain-specific verbs/actions
371
- *
372
- * This is for state transitions and side effects:
373
- * - invoice.send, invoice.pay, invoice.void
374
- * - document.publish, document.archive
375
- * - order.fulfill, order.refund
376
- *
377
- * Can be scoped: act:* (all), act:send, act:pay, etc.
378
- */
379
- act: (resourceType: string, verbs?: string[]): Permission => ({
380
- name: 'act',
381
- description: verbs
382
- ? `Perform ${verbs.join(', ')} on ${resourceType}`
383
- : `Perform actions on ${resourceType}`,
384
- resourceType,
385
- actions: verbs || ['*'], // '*' means all verbs
386
- inheritable: true,
387
- }),
388
-
389
- delete: (resourceType: string): Permission => ({
390
- name: 'delete',
391
- description: `Delete ${resourceType}`,
392
- resourceType,
393
- actions: ['delete', 'remove', 'destroy'],
394
- inheritable: false, // Usually not inherited
395
- }),
396
-
397
- manage: (resourceType: string): Permission => ({
398
- name: 'manage',
399
- description: `Full management of ${resourceType}`,
400
- resourceType,
401
- actions: ['*'], // All actions
402
- inheritable: true,
403
- }),
404
- }
405
-
406
- /**
407
- * @deprecated Use StandardPermissions instead
408
- */
409
- export const CRUDPermissions = StandardPermissions
410
-
411
- // =============================================================================
412
- // Verb-Scoped Permissions (e.g., invoice.pay, document.publish)
413
- // =============================================================================
414
-
415
- /**
416
- * Create a verb-scoped permission
417
- *
418
- * @example
419
- * ```ts
420
- * // Single verb
421
- * const canPay = verbPermission('invoice', 'pay')
422
- * // { name: 'invoice.pay', actions: ['pay'], resourceType: 'invoice' }
423
- *
424
- * // Multiple verbs
425
- * const canManagePayments = verbPermission('invoice', ['send', 'pay', 'void', 'refund'])
426
- * ```
427
- */
428
- export function verbPermission(
429
- resourceType: string,
430
- verbs: string | string[],
431
- options?: { inheritable?: boolean; description?: string }
432
- ): Permission {
433
- const verbList = Array.isArray(verbs) ? verbs : [verbs]
434
- const name = verbList.length === 1
435
- ? `${resourceType}.${verbList[0]}`
436
- : `${resourceType}.[${verbList.join(',')}]`
437
-
438
- return {
439
- name,
440
- description: options?.description || `Can ${verbList.join(', ')} ${resourceType}`,
441
- resourceType,
442
- actions: verbList,
443
- inheritable: options?.inheritable ?? true,
444
- }
445
- }
446
-
447
- /**
448
- * Create permissions from a Noun's actions
449
- *
450
- * @example
451
- * ```ts
452
- * const invoicePerms = nounPermissions(InvoiceNoun)
453
- * // Creates: invoice.create, invoice.send, invoice.pay, invoice.void, etc.
454
- * ```
455
- */
456
- export function nounPermissions(noun: Noun): Permission[] {
457
- const resourceType = noun.singular
458
- const permissions: Permission[] = []
459
-
460
- // Standard CRUD
461
- permissions.push(StandardPermissions.read(resourceType))
462
- permissions.push(StandardPermissions.edit(resourceType))
463
- permissions.push(StandardPermissions.delete(resourceType))
464
-
465
- // Domain-specific verbs from noun.actions
466
- if (noun.actions) {
467
- for (const action of noun.actions) {
468
- const verb = typeof action === 'string' ? action : action.action
469
- // Skip standard CRUD verbs (already covered)
470
- if (['create', 'read', 'update', 'delete', 'get', 'list'].includes(verb)) continue
471
-
472
- permissions.push(verbPermission(resourceType, verb))
473
- }
474
- }
475
-
476
- return permissions
477
- }
478
-
479
- /**
480
- * Permission pattern matching
481
- *
482
- * Checks if an action matches a permission pattern.
483
- *
484
- * @example
485
- * ```ts
486
- * matchesPermission('pay', ['*']) // true - wildcard
487
- * matchesPermission('pay', ['pay']) // true - exact
488
- * matchesPermission('pay', ['send', 'pay']) // true - in list
489
- * matchesPermission('pay', ['send']) // false
490
- * ```
491
- */
492
- export function matchesPermission(action: string, allowedActions: string[]): boolean {
493
- if (allowedActions.includes('*')) return true
494
- if (allowedActions.includes(action)) return true
495
-
496
- // Check for prefix patterns like 'invoice.*' matching 'invoice.pay'
497
- for (const pattern of allowedActions) {
498
- if (pattern.endsWith('.*')) {
499
- const prefix = pattern.slice(0, -2)
500
- if (action.startsWith(prefix + '.')) return true
501
- }
502
- }
503
-
504
- return false
505
- }
506
-
507
- /**
508
- * Create standard roles for a resource type
509
- */
510
- export function createStandardRoles(resourceType: string): Record<RoleLevel, Role> {
511
- return {
512
- owner: {
513
- id: `${resourceType}:owner`,
514
- name: 'Owner',
515
- description: `Full control of ${resourceType}, including deletion and transfer`,
516
- resourceType,
517
- permissions: [
518
- CRUDPermissions.manage(resourceType),
519
- {
520
- name: 'transfer',
521
- description: 'Transfer ownership',
522
- resourceType,
523
- actions: ['transfer'],
524
- inheritable: false,
525
- },
526
- ],
527
- },
528
-
529
- admin: {
530
- id: `${resourceType}:admin`,
531
- name: 'Admin',
532
- description: `Administrative access to ${resourceType}`,
533
- resourceType,
534
- permissions: [
535
- StandardPermissions.create(resourceType),
536
- StandardPermissions.read(resourceType),
537
- StandardPermissions.edit(resourceType),
538
- StandardPermissions.act(resourceType),
539
- StandardPermissions.delete(resourceType),
540
- ],
541
- },
542
-
543
- editor: {
544
- id: `${resourceType}:editor`,
545
- name: 'Editor',
546
- description: `Can edit ${resourceType}`,
547
- resourceType,
548
- permissions: [
549
- StandardPermissions.read(resourceType),
550
- StandardPermissions.edit(resourceType),
551
- StandardPermissions.act(resourceType),
552
- ],
553
- },
554
-
555
- viewer: {
556
- id: `${resourceType}:viewer`,
557
- name: 'Viewer',
558
- description: `Read-only access to ${resourceType}`,
559
- resourceType,
560
- permissions: [
561
- StandardPermissions.read(resourceType),
562
- ],
563
- },
564
-
565
- guest: {
566
- id: `${resourceType}:guest`,
567
- name: 'Guest',
568
- description: `Limited access to ${resourceType}`,
569
- resourceType,
570
- permissions: [
571
- {
572
- name: 'view',
573
- description: 'View basic info',
574
- resourceType,
575
- actions: ['get'],
576
- inheritable: false,
577
- },
578
- ],
579
- },
580
- }
581
- }
582
-
583
- // =============================================================================
584
- // Authorization Schema (integrates with Noun)
585
- // =============================================================================
586
-
587
- /**
588
- * Authorization schema for a Noun
589
- *
590
- * Extends a Noun with authorization metadata.
591
- */
592
- export interface AuthorizedNoun extends Noun {
593
- /** Resource type configuration */
594
- authorization?: {
595
- /** Parent resource type in hierarchy */
596
- parentType?: string
597
-
598
- /** Available roles for this resource type */
599
- roles?: Role[]
600
-
601
- /** Custom permissions beyond CRUD */
602
- permissions?: Permission[]
603
-
604
- /** Whether this is a high-cardinality type */
605
- local?: boolean
606
-
607
- /** Default role for new resources */
608
- defaultRole?: string
609
- }
610
- }
611
-
612
- /**
613
- * Add authorization to a Noun
614
- */
615
- export function authorizeNoun(
616
- noun: Noun,
617
- config: AuthorizedNoun['authorization']
618
- ): AuthorizedNoun {
619
- return {
620
- ...noun,
621
- authorization: config,
622
- }
623
- }
624
-
625
- // =============================================================================
626
- // Authorization Engine Interface
627
- // =============================================================================
628
-
629
- /**
630
- * Authorization engine interface
631
- *
632
- * Implemented by providers (WorkOS, in-memory, custom).
633
- */
634
- export interface AuthorizationEngine {
635
- // Resource management
636
- createResource(resource: Resource): Promise<Resource>
637
- getResource(ref: ResourceRef): Promise<Resource | null>
638
- deleteResource(ref: ResourceRef): Promise<void>
639
- listResources(type: string, parentRef?: ResourceRef): Promise<Resource[]>
640
-
641
- // Assignment management
642
- assign(input: AssignmentInput): Promise<Assignment>
643
- unassign(assignmentId: string): Promise<void>
644
- getAssignment(id: string): Promise<Assignment | null>
645
- listAssignments(filter: {
646
- subject?: Subject
647
- role?: string
648
- resource?: ResourceRef
649
- }): Promise<Assignment[]>
650
-
651
- // Authorization checks
652
- check(request: AuthzCheckRequest): Promise<AuthzCheckResult>
653
- batchCheck(request: AuthzBatchCheckRequest): Promise<AuthzBatchCheckResult>
654
-
655
- // Discovery
656
- listSubjectsWithAccess(resource: ResourceRef, action?: string): Promise<Subject[]>
657
- listResourcesForSubject(subject: Subject, resourceType: string, action?: string): Promise<Resource[]>
658
- }
659
-
660
- // =============================================================================
661
- // Helper Functions
662
- // =============================================================================
663
-
664
- /**
665
- * Parse subject string to Subject object
666
- *
667
- * @example
668
- * ```ts
669
- * parseSubject('user:123') // { type: 'user', id: '123' }
670
- * parseSubject('group:admins') // { type: 'group', id: 'admins' }
671
- * ```
672
- */
673
- export function parseSubject(str: string): Subject {
674
- const [type, id] = str.split(':')
675
- if (!type || !id) {
676
- throw new Error(`Invalid subject format: ${str}. Expected 'type:id'`)
677
- }
678
- return { type, id }
679
- }
680
-
681
- /**
682
- * Format Subject as string
683
- */
684
- export function formatSubject(subject: Subject): string {
685
- return `${subject.type}:${subject.id}`
686
- }
687
-
688
- /**
689
- * Parse resource string to ResourceRef
690
- *
691
- * @example
692
- * ```ts
693
- * parseResource('workspace:456') // { type: 'workspace', id: '456' }
694
- * ```
695
- */
696
- export function parseResource(str: string): ResourceRef {
697
- const [type, id] = str.split(':')
698
- if (!type || !id) {
699
- throw new Error(`Invalid resource format: ${str}. Expected 'type:id'`)
700
- }
701
- return { type, id }
702
- }
703
-
704
- /**
705
- * Format ResourceRef as string
706
- */
707
- export function formatResource(resource: ResourceRef): string {
708
- return `${resource.type}:${resource.id}`
709
- }
710
-
711
- /**
712
- * Check if a subject matches another (for assignment matching)
713
- */
714
- export function subjectMatches(a: Subject, b: Subject): boolean {
715
- return a.type === b.type && a.id === b.id
716
- }
717
-
718
- /**
719
- * Check if a resource matches another
720
- */
721
- export function resourceMatches(a: ResourceRef, b: ResourceRef): boolean {
722
- return a.type === b.type && a.id === b.id
723
- }
724
-
725
- // =============================================================================
726
- // In-Memory Authorization Engine (for testing/development)
727
- // =============================================================================
728
-
729
- /**
730
- * In-memory authorization engine
731
- *
732
- * Simple implementation for testing and development.
733
- * For production, use WorkOS or a persistent provider.
734
- */
735
- export class InMemoryAuthorizationEngine implements AuthorizationEngine {
736
- private resources = new Map<string, Resource>()
737
- private assignments = new Map<string, Assignment>()
738
- private roles = new Map<string, Role>()
739
- private hierarchy: ResourceHierarchy
740
-
741
- constructor(config?: { hierarchy?: ResourceHierarchy; roles?: Role[] }) {
742
- this.hierarchy = config?.hierarchy || StandardHierarchies.saas
743
- if (config?.roles) {
744
- for (const role of config.roles) {
745
- this.roles.set(role.id, role)
746
- }
747
- }
748
- }
749
-
750
- // Resource management
751
- async createResource(resource: Resource): Promise<Resource> {
752
- const key = formatResource(resource)
753
- this.resources.set(key, resource)
754
- return resource
755
- }
756
-
757
- async getResource(ref: ResourceRef): Promise<Resource | null> {
758
- return this.resources.get(formatResource(ref)) || null
759
- }
760
-
761
- async deleteResource(ref: ResourceRef): Promise<void> {
762
- this.resources.delete(formatResource(ref))
763
- }
764
-
765
- async listResources(type: string, parentRef?: ResourceRef): Promise<Resource[]> {
766
- const results: Resource[] = []
767
- for (const resource of this.resources.values()) {
768
- if (resource.type !== type) continue
769
- if (parentRef && (!resource.parent || !resourceMatches(resource.parent, parentRef))) continue
770
- results.push(resource)
771
- }
772
- return results
773
- }
774
-
775
- // Assignment management
776
- async assign(input: AssignmentInput): Promise<Assignment> {
777
- const subject = typeof input.subject === 'string'
778
- ? parseSubject(input.subject)
779
- : input.subject
780
- const resource = typeof input.resource === 'string'
781
- ? parseResource(input.resource)
782
- : input.resource
783
-
784
- const assignment: Assignment = {
785
- id: `${formatSubject(subject)}:${input.role}:${formatResource(resource)}`,
786
- subject,
787
- role: input.role,
788
- resource,
789
- createdAt: new Date(),
790
- expiresAt: input.expiresAt,
791
- metadata: input.metadata,
792
- }
793
-
794
- this.assignments.set(assignment.id, assignment)
795
- return assignment
796
- }
797
-
798
- async unassign(assignmentId: string): Promise<void> {
799
- this.assignments.delete(assignmentId)
800
- }
801
-
802
- async getAssignment(id: string): Promise<Assignment | null> {
803
- return this.assignments.get(id) || null
804
- }
805
-
806
- async listAssignments(filter: {
807
- subject?: Subject
808
- role?: string
809
- resource?: ResourceRef
810
- }): Promise<Assignment[]> {
811
- const results: Assignment[] = []
812
- for (const assignment of this.assignments.values()) {
813
- if (filter.subject && !subjectMatches(assignment.subject, filter.subject)) continue
814
- if (filter.role && assignment.role !== filter.role) continue
815
- if (filter.resource && !resourceMatches(assignment.resource, filter.resource)) continue
816
- results.push(assignment)
817
- }
818
- return results
819
- }
820
-
821
- // Authorization checks
822
- async check(request: AuthzCheckRequest): Promise<AuthzCheckResult> {
823
- const start = Date.now()
824
- const subject = typeof request.subject === 'string'
825
- ? parseSubject(request.subject)
826
- : request.subject
827
- const resource = typeof request.resource === 'string'
828
- ? parseResource(request.resource)
829
- : request.resource
830
-
831
- // Check direct assignments
832
- const assignments = await this.listAssignments({ subject })
833
-
834
- for (const assignment of assignments) {
835
- // Check if assignment is on this resource or a parent
836
- if (this.resourceInScope(resource, assignment.resource)) {
837
- const role = this.roles.get(assignment.role)
838
- if (role && this.roleGrantsAction(role, request.action, resource.type)) {
839
- return {
840
- allowed: true,
841
- reason: `Granted by role '${role.name}' on ${formatResource(assignment.resource)}`,
842
- assignment,
843
- latencyMs: Date.now() - start,
844
- }
845
- }
846
- }
847
- }
848
-
849
- return {
850
- allowed: false,
851
- reason: 'No matching assignment found',
852
- latencyMs: Date.now() - start,
853
- }
854
- }
855
-
856
- async batchCheck(request: AuthzBatchCheckRequest): Promise<AuthzBatchCheckResult> {
857
- const start = Date.now()
858
- const results = await Promise.all(request.checks.map(c => this.check(c)))
859
- return {
860
- results,
861
- latencyMs: Date.now() - start,
862
- }
863
- }
864
-
865
- // Discovery
866
- async listSubjectsWithAccess(resource: ResourceRef, action?: string): Promise<Subject[]> {
867
- const subjects: Subject[] = []
868
- const seen = new Set<string>()
869
-
870
- for (const assignment of this.assignments.values()) {
871
- if (!this.resourceInScope(resource, assignment.resource)) continue
872
-
873
- const role = this.roles.get(assignment.role)
874
- if (action && role && !this.roleGrantsAction(role, action, resource.type)) continue
875
-
876
- const key = formatSubject(assignment.subject)
877
- if (!seen.has(key)) {
878
- seen.add(key)
879
- subjects.push(assignment.subject)
880
- }
881
- }
882
-
883
- return subjects
884
- }
885
-
886
- async listResourcesForSubject(
887
- subject: Subject,
888
- resourceType: string,
889
- action?: string
890
- ): Promise<Resource[]> {
891
- const resources: Resource[] = []
892
- const assignments = await this.listAssignments({ subject })
893
-
894
- for (const assignment of assignments) {
895
- const role = this.roles.get(assignment.role)
896
- if (!role) continue
897
- if (action && !this.roleGrantsAction(role, action, resourceType)) continue
898
-
899
- // Find all resources of the type that are in scope
900
- for (const resource of this.resources.values()) {
901
- if (resource.type !== resourceType) continue
902
- if (this.resourceInScope(resource, assignment.resource)) {
903
- resources.push(resource)
904
- }
905
- }
906
- }
907
-
908
- return resources
909
- }
910
-
911
- // Helpers
912
- private resourceInScope(target: ResourceRef, scope: ResourceRef): boolean {
913
- // Same resource
914
- if (resourceMatches(target, scope)) return true
915
-
916
- // Check if scope is a parent of target
917
- const targetResource = this.resources.get(formatResource(target))
918
- if (!targetResource?.parent) return false
919
-
920
- return this.resourceInScope(targetResource.parent, scope)
921
- }
922
-
923
- private roleGrantsAction(role: Role, action: string, resourceType: string): boolean {
924
- for (const permission of role.permissions) {
925
- // Check resource type match (or inheritable)
926
- if (permission.resourceType !== resourceType && !permission.inheritable) continue
927
-
928
- // Check action match
929
- if (permission.actions.includes(action) || permission.actions.includes('*')) {
930
- return true
931
- }
932
- }
933
-
934
- // Check inherited roles
935
- if (role.inherits) {
936
- for (const inheritedRoleId of role.inherits) {
937
- const inheritedRole = this.roles.get(inheritedRoleId)
938
- if (inheritedRole && this.roleGrantsAction(inheritedRole, action, resourceType)) {
939
- return true
940
- }
941
- }
942
- }
943
-
944
- return false
945
- }
946
-
947
- // Role management
948
- registerRole(role: Role): void {
949
- this.roles.set(role.id, role)
950
- }
951
-
952
- getRole(id: string): Role | undefined {
953
- return this.roles.get(id)
954
- }
955
- }
956
-
957
- // =============================================================================
958
- // Business Role Integration
959
- // =============================================================================
960
-
961
- /**
962
- * Business role - organizational role (CEO, Engineer, Manager)
963
- *
964
- * Different from authorization Role - this represents a job function.
965
- * Can be linked to authorization Roles for permissions.
966
- */
967
- export interface BusinessRole {
968
- /** Role identifier */
969
- id: string
970
-
971
- /** Display name (CEO, Software Engineer, Product Manager) */
972
- name: string
973
-
974
- /** Description */
975
- description?: string
976
-
977
- /** Department */
978
- department?: string
979
-
980
- /** Level in organization (1 = IC, 2 = Manager, 3 = Director, 4 = VP, 5 = C-level) */
981
- level?: number
982
-
983
- /** Reports to (parent role) */
984
- reportsTo?: string
985
-
986
- /** Responsibilities */
987
- responsibilities?: string[]
988
-
989
- /** Required skills */
990
- skills?: string[]
991
-
992
- /** Authorization roles this business role grants */
993
- authorizationRoles?: string[]
994
-
995
- /** Metadata */
996
- metadata?: Record<string, unknown>
997
- }
998
-
999
- /**
1000
- * Link business roles to authorization roles
1001
- */
1002
- export function linkBusinessRole(
1003
- businessRole: BusinessRole,
1004
- authRoles: string[]
1005
- ): BusinessRole {
1006
- return {
1007
- ...businessRole,
1008
- authorizationRoles: [
1009
- ...(businessRole.authorizationRoles || []),
1010
- ...authRoles,
1011
- ],
1012
- }
1013
- }
1014
-
1015
- // =============================================================================
1016
- // Noun Definition for Role (makes Role a first-class entity)
1017
- // =============================================================================
1018
-
1019
- /**
1020
- * Role as a Noun - can be stored in ai-database
1021
- */
1022
- export const RoleNoun: Noun = {
1023
- singular: 'role',
1024
- plural: 'roles',
1025
- description: 'An authorization role with permissions',
1026
-
1027
- properties: {
1028
- id: { type: 'string', description: 'Unique role identifier' },
1029
- name: { type: 'string', description: 'Display name' },
1030
- description: { type: 'string', optional: true, description: 'Role description' },
1031
- resourceType: { type: 'string', description: 'Resource type this role is scoped to' },
1032
- level: { type: 'string', optional: true, description: 'Role level (owner, admin, editor, viewer, guest)' },
1033
- },
1034
-
1035
- relationships: {
1036
- permissions: { type: 'Permission[]', description: 'Permissions granted by this role' },
1037
- inherits: { type: 'Role[]', description: 'Parent roles inherited from' },
1038
- assignments: { type: 'Assignment[]', backref: 'role', description: 'Assignments using this role' },
1039
- },
1040
-
1041
- actions: ['create', 'update', 'delete', 'assign', 'unassign'],
1042
- events: ['created', 'updated', 'deleted', 'assigned', 'unassigned'],
1043
- }
1044
-
1045
- /**
1046
- * Assignment as a Noun
1047
- */
1048
- export const AssignmentNoun: Noun = {
1049
- singular: 'assignment',
1050
- plural: 'assignments',
1051
- description: 'A role assignment binding subject, role, and resource',
1052
-
1053
- properties: {
1054
- id: { type: 'string', description: 'Unique assignment identifier' },
1055
- subjectType: { type: 'string', description: 'Subject type (user, group, service, agent)' },
1056
- subjectId: { type: 'string', description: 'Subject identifier' },
1057
- roleId: { type: 'string', description: 'Role identifier' },
1058
- resourceType: { type: 'string', description: 'Resource type' },
1059
- resourceId: { type: 'string', description: 'Resource identifier' },
1060
- expiresAt: { type: 'datetime', optional: true, description: 'Expiration timestamp' },
1061
- },
1062
-
1063
- relationships: {
1064
- role: { type: 'Role', backref: 'assignments', description: 'The assigned role' },
1065
- },
1066
-
1067
- actions: ['create', 'delete', 'extend', 'revoke'],
1068
- events: ['created', 'deleted', 'extended', 'revoked', 'expired'],
1069
- }
1070
-
1071
- /**
1072
- * Permission as a Noun
1073
- */
1074
- export const PermissionNoun: Noun = {
1075
- singular: 'permission',
1076
- plural: 'permissions',
1077
- description: 'A permission granting actions on a resource type',
1078
-
1079
- properties: {
1080
- name: { type: 'string', description: 'Permission name' },
1081
- description: { type: 'string', optional: true, description: 'Permission description' },
1082
- resourceType: { type: 'string', description: 'Resource type this applies to' },
1083
- actions: { type: 'string', array: true, description: 'Actions granted' },
1084
- inheritable: { type: 'boolean', optional: true, description: 'Whether permission flows to children' },
1085
- },
1086
-
1087
- relationships: {
1088
- roles: { type: 'Role[]', backref: 'permissions', description: 'Roles that include this permission' },
1089
- },
1090
-
1091
- actions: ['create', 'update', 'delete'],
1092
- events: ['created', 'updated', 'deleted'],
1093
- }
1094
-
1095
- /**
1096
- * All authorization-related Nouns
1097
- */
1098
- export const AuthorizationNouns = {
1099
- Role: RoleNoun,
1100
- Assignment: AssignmentNoun,
1101
- Permission: PermissionNoun,
1102
- }