@openhi/constructs 0.0.92 → 0.0.93

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.
@@ -23,30 +23,904 @@ __export(pre_token_generation_handler_exports, {
23
23
  handler: () => handler
24
24
  });
25
25
  module.exports = __toCommonJS(pre_token_generation_handler_exports);
26
- var OPENHI_CLAIMS = {
27
- ohi_tid: "placeholder-tenant-id",
28
- ohi_wid: "placeholder-workspace-id",
29
- ohi_uid: "placeholder-user-id",
30
- ohi_uname: "placeholder"
26
+
27
+ // src/data/dynamo/dynamo-control-service.ts
28
+ var import_electrodb8 = require("electrodb");
29
+
30
+ // src/data/dynamo/dynamo-client.ts
31
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
32
+ var defaultTableName = process.env.DYNAMO_TABLE_NAME ?? "jesttesttable";
33
+ var dynamoClient = new import_client_dynamodb.DynamoDBClient({
34
+ ...process.env.MOCK_DYNAMODB_ENDPOINT && {
35
+ endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
36
+ sslEnabled: false,
37
+ region: "local"
38
+ }
39
+ });
40
+
41
+ // src/data/dynamo/entities/control/configuration-entity.ts
42
+ var import_electrodb = require("electrodb");
43
+
44
+ // src/data/dynamo/shard.ts
45
+ var SHARD_COUNT = 4;
46
+ function computeShard(id) {
47
+ let hash = 2166136261;
48
+ for (let i = 0; i < id.length; i++) {
49
+ hash ^= id.charCodeAt(i);
50
+ hash = Math.imul(hash, 16777619);
51
+ }
52
+ return (hash >>> 0) % SHARD_COUNT;
53
+ }
54
+
55
+ // src/data/dynamo/entities/control/control-entity-common.ts
56
+ var gsi1ShardAttribute = {
57
+ type: "string",
58
+ watch: ["id"],
59
+ set: (_val, item) => {
60
+ if (typeof item?.id !== "string" || item.id.length === 0) {
61
+ return void 0;
62
+ }
63
+ return String(computeShard(item.id));
64
+ }
65
+ };
66
+
67
+ // src/data/dynamo/entities/control/configuration-entity.ts
68
+ var ConfigurationEntity = new import_electrodb.Entity({
69
+ model: {
70
+ entity: "configuration",
71
+ service: "control",
72
+ version: "01"
73
+ },
74
+ attributes: {
75
+ /** Sort key. "CURRENT" for current version; version history in S3. */
76
+ sk: {
77
+ type: "string",
78
+ required: true,
79
+ default: "CURRENT"
80
+ },
81
+ /** Tenant scope. Use "BASELINE" when the config is baseline default (no tenant). */
82
+ tenantId: {
83
+ type: "string",
84
+ required: true,
85
+ default: "BASELINE"
86
+ },
87
+ /** Workspace scope. Use "-" when absent. */
88
+ workspaceId: {
89
+ type: "string",
90
+ required: true,
91
+ default: "-"
92
+ },
93
+ /** User scope. Use "-" when absent. */
94
+ userId: {
95
+ type: "string",
96
+ required: true,
97
+ default: "-"
98
+ },
99
+ /** Role scope. Use "-" when absent. */
100
+ roleId: {
101
+ type: "string",
102
+ required: true,
103
+ default: "-"
104
+ },
105
+ /** Config type (category), e.g. endpoints, branding, display. */
106
+ key: {
107
+ type: "string",
108
+ required: true
109
+ },
110
+ /** FHIR Resource.id; logical id in URL and for the Configuration resource. */
111
+ id: {
112
+ type: "string",
113
+ required: true
114
+ },
115
+ /** Payload as JSON string. JSON.stringify(resource) on write; JSON.parse(item.resource) on read. */
116
+ resource: {
117
+ type: "string",
118
+ required: true
119
+ },
120
+ /**
121
+ * Summary projection (key display fields as JSON string: id, key, status).
122
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
123
+ */
124
+ summary: {
125
+ type: "string",
126
+ required: true
127
+ },
128
+ /** Version id (e.g. ULID). Tracks current version; S3 history key. */
129
+ vid: {
130
+ type: "string",
131
+ required: true
132
+ },
133
+ lastUpdated: {
134
+ type: "string",
135
+ required: true
136
+ },
137
+ gsi1Shard: gsi1ShardAttribute,
138
+ deleted: {
139
+ type: "boolean",
140
+ required: false
141
+ },
142
+ bundleId: {
143
+ type: "string",
144
+ required: false
145
+ },
146
+ msgId: {
147
+ type: "string",
148
+ required: false
149
+ }
150
+ },
151
+ indexes: {
152
+ /** Base table: PK, SK (data store key names). PK is built from tenantId, workspaceId, userId, roleId; SK is built from key and sk. Do not supply PK or SK from outside. */
153
+ record: {
154
+ pk: {
155
+ field: "PK",
156
+ composite: ["tenantId", "workspaceId", "userId", "roleId"],
157
+ template: "CONFIG#TID#${tenantId}#WID#${workspaceId}#UID#${userId}#RID#${roleId}"
158
+ },
159
+ sk: {
160
+ field: "SK",
161
+ composite: ["key", "sk"],
162
+ template: "KEY#${key}#SK#${sk}"
163
+ }
164
+ },
165
+ /**
166
+ * GSI1 — Unified Sharded List per ADR-011: list all Configuration entries for a
167
+ * (tenant, workspace) across the four shards. Use for "list configs scoped to this tenant"
168
+ * (workspaceId = "-") or "list configs scoped to this workspace". Does not support
169
+ * hierarchical resolution in one query; use base table GetItem in fallback order
170
+ * (user → workspace → tenant → baseline) for that.
171
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
172
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
173
+ */
174
+ gsi1: {
175
+ index: "GSI1",
176
+ pk: {
177
+ field: "GSI1PK",
178
+ composite: ["tenantId", "workspaceId", "gsi1Shard"],
179
+ template: "TID#${tenantId}#WID#${workspaceId}#RT#Configuration#SHARD#${gsi1Shard}"
180
+ },
181
+ sk: {
182
+ field: "GSI1SK",
183
+ casing: "none",
184
+ composite: ["lastUpdated", "id"],
185
+ template: "${lastUpdated}#${id}"
186
+ }
187
+ }
188
+ }
189
+ });
190
+
191
+ // src/data/dynamo/entities/control/membership-entity.ts
192
+ var import_electrodb2 = require("electrodb");
193
+ var MembershipEntity = new import_electrodb2.Entity({
194
+ model: {
195
+ entity: "membership",
196
+ service: "control",
197
+ version: "01"
198
+ },
199
+ attributes: {
200
+ /** Sort key sentinel. Always "CURRENT". */
201
+ sk: {
202
+ type: "string",
203
+ required: true,
204
+ default: "CURRENT"
205
+ },
206
+ /** Tenant in which the user has membership (required). */
207
+ tenantId: {
208
+ type: "string",
209
+ required: true
210
+ },
211
+ /** FHIR Resource.id; membership id. */
212
+ id: {
213
+ type: "string",
214
+ required: true
215
+ },
216
+ /** Full Membership resource serialized as JSON string. */
217
+ resource: {
218
+ type: "string",
219
+ required: true
220
+ },
221
+ /**
222
+ * Summary projection (key display fields as JSON string: id, displayName, status).
223
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
224
+ */
225
+ summary: {
226
+ type: "string",
227
+ required: true
228
+ },
229
+ /** Version id (e.g. ULID). */
230
+ vid: {
231
+ type: "string",
232
+ required: true
233
+ },
234
+ lastUpdated: {
235
+ type: "string",
236
+ required: true
237
+ },
238
+ gsi1Shard: gsi1ShardAttribute,
239
+ deleted: {
240
+ type: "boolean",
241
+ required: false
242
+ },
243
+ bundleId: {
244
+ type: "string",
245
+ required: false
246
+ },
247
+ msgId: {
248
+ type: "string",
249
+ required: false
250
+ }
251
+ },
252
+ indexes: {
253
+ /** Base table: PK = TID#<tenantId>#MEMBERSHIP#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
254
+ record: {
255
+ pk: {
256
+ field: "PK",
257
+ composite: ["tenantId", "id"],
258
+ template: "TID#${tenantId}#MEMBERSHIP#ID#${id}"
259
+ },
260
+ sk: {
261
+ field: "SK",
262
+ composite: ["sk"],
263
+ template: "${sk}"
264
+ }
265
+ },
266
+ /**
267
+ * GSI1 — Unified Sharded List per ADR-011: list all Memberships for a tenant across the
268
+ * four shards. Membership is tenant-scoped only, so `WID#-` is a sentinel.
269
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
270
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
271
+ */
272
+ gsi1: {
273
+ index: "GSI1",
274
+ pk: {
275
+ field: "GSI1PK",
276
+ composite: ["tenantId", "gsi1Shard"],
277
+ template: "TID#${tenantId}#WID#-#RT#Membership#SHARD#${gsi1Shard}"
278
+ },
279
+ sk: {
280
+ field: "GSI1SK",
281
+ casing: "none",
282
+ composite: ["lastUpdated", "id"],
283
+ template: "${lastUpdated}#${id}"
284
+ }
285
+ }
286
+ }
287
+ });
288
+
289
+ // src/data/dynamo/entities/control/role-entity.ts
290
+ var import_electrodb3 = require("electrodb");
291
+ var RoleEntity = new import_electrodb3.Entity({
292
+ model: {
293
+ entity: "role",
294
+ service: "control",
295
+ version: "01"
296
+ },
297
+ attributes: {
298
+ /** Sort key sentinel. Always "CURRENT". */
299
+ sk: {
300
+ type: "string",
301
+ required: true,
302
+ default: "CURRENT"
303
+ },
304
+ /** FHIR Resource.id; role id. */
305
+ id: {
306
+ type: "string",
307
+ required: true
308
+ },
309
+ /** Full Role resource serialized as JSON string. */
310
+ resource: {
311
+ type: "string",
312
+ required: true
313
+ },
314
+ /**
315
+ * Summary projection (key display fields as JSON string: id, displayName, status).
316
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
317
+ */
318
+ summary: {
319
+ type: "string",
320
+ required: true
321
+ },
322
+ /** Version id (e.g. ULID). */
323
+ vid: {
324
+ type: "string",
325
+ required: true
326
+ },
327
+ lastUpdated: {
328
+ type: "string",
329
+ required: true
330
+ },
331
+ gsi1Shard: gsi1ShardAttribute,
332
+ deleted: {
333
+ type: "boolean",
334
+ required: false
335
+ },
336
+ bundleId: {
337
+ type: "string",
338
+ required: false
339
+ },
340
+ msgId: {
341
+ type: "string",
342
+ required: false
343
+ }
344
+ },
345
+ indexes: {
346
+ /** Base table: PK = ROLE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
347
+ record: {
348
+ pk: {
349
+ field: "PK",
350
+ composite: ["id"],
351
+ template: "ROLE#ID#${id}"
352
+ },
353
+ sk: {
354
+ field: "SK",
355
+ composite: ["sk"],
356
+ template: "${sk}"
357
+ }
358
+ },
359
+ /**
360
+ * GSI1 — Unified Sharded List per ADR-011: list all Roles across the four shards.
361
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#Role#SHARD#<n>`.
362
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
363
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
364
+ */
365
+ gsi1: {
366
+ index: "GSI1",
367
+ pk: {
368
+ field: "GSI1PK",
369
+ composite: ["gsi1Shard"],
370
+ template: "TID#-#WID#-#RT#Role#SHARD#${gsi1Shard}"
371
+ },
372
+ sk: {
373
+ field: "GSI1SK",
374
+ casing: "none",
375
+ composite: ["lastUpdated", "id"],
376
+ template: "${lastUpdated}#${id}"
377
+ }
378
+ }
379
+ }
380
+ });
381
+
382
+ // src/data/dynamo/entities/control/roleassignment-entity.ts
383
+ var import_electrodb4 = require("electrodb");
384
+ var RoleAssignmentEntity = new import_electrodb4.Entity({
385
+ model: {
386
+ entity: "roleassignment",
387
+ service: "control",
388
+ version: "01"
389
+ },
390
+ attributes: {
391
+ /** Sort key sentinel. Always "CURRENT". */
392
+ sk: {
393
+ type: "string",
394
+ required: true,
395
+ default: "CURRENT"
396
+ },
397
+ /** Tenant in which the role assignment applies (required). */
398
+ tenantId: {
399
+ type: "string",
400
+ required: true
401
+ },
402
+ /** FHIR Resource.id; role assignment id. */
403
+ id: {
404
+ type: "string",
405
+ required: true
406
+ },
407
+ /** Full RoleAssignment resource serialized as JSON string. */
408
+ resource: {
409
+ type: "string",
410
+ required: true
411
+ },
412
+ /**
413
+ * Summary projection (key display fields as JSON string: id, displayName, status).
414
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
415
+ */
416
+ summary: {
417
+ type: "string",
418
+ required: true
419
+ },
420
+ /** Version id (e.g. ULID). */
421
+ vid: {
422
+ type: "string",
423
+ required: true
424
+ },
425
+ lastUpdated: {
426
+ type: "string",
427
+ required: true
428
+ },
429
+ gsi1Shard: gsi1ShardAttribute,
430
+ deleted: {
431
+ type: "boolean",
432
+ required: false
433
+ },
434
+ bundleId: {
435
+ type: "string",
436
+ required: false
437
+ },
438
+ msgId: {
439
+ type: "string",
440
+ required: false
441
+ }
442
+ },
443
+ indexes: {
444
+ /** Base table: PK = TID#<tenantId>#ROLEASSIGNMENT#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
445
+ record: {
446
+ pk: {
447
+ field: "PK",
448
+ composite: ["tenantId", "id"],
449
+ template: "TID#${tenantId}#ROLEASSIGNMENT#ID#${id}"
450
+ },
451
+ sk: {
452
+ field: "SK",
453
+ composite: ["sk"],
454
+ template: "${sk}"
455
+ }
456
+ },
457
+ /**
458
+ * GSI1 — Unified Sharded List per ADR-011: list all RoleAssignments for a tenant across the
459
+ * four shards. Tenant-scoped only, so `WID#-` is a sentinel.
460
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
461
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
462
+ */
463
+ gsi1: {
464
+ index: "GSI1",
465
+ pk: {
466
+ field: "GSI1PK",
467
+ composite: ["tenantId", "gsi1Shard"],
468
+ template: "TID#${tenantId}#WID#-#RT#RoleAssignment#SHARD#${gsi1Shard}"
469
+ },
470
+ sk: {
471
+ field: "GSI1SK",
472
+ casing: "none",
473
+ composite: ["lastUpdated", "id"],
474
+ template: "${lastUpdated}#${id}"
475
+ }
476
+ }
477
+ }
478
+ });
479
+
480
+ // src/data/dynamo/entities/control/tenant-entity.ts
481
+ var import_electrodb5 = require("electrodb");
482
+ var TenantEntity = new import_electrodb5.Entity({
483
+ model: {
484
+ entity: "tenant",
485
+ service: "control",
486
+ version: "01"
487
+ },
488
+ attributes: {
489
+ /** Sort key sentinel. Always "CURRENT". */
490
+ sk: {
491
+ type: "string",
492
+ required: true,
493
+ default: "CURRENT"
494
+ },
495
+ /** The tenant's own id (= resource id). Drives the partition key. */
496
+ tenantId: {
497
+ type: "string",
498
+ required: true
499
+ },
500
+ /** FHIR Resource.id; logical id in URL. Equals tenantId. */
501
+ id: {
502
+ type: "string",
503
+ required: true
504
+ },
505
+ /** Full Tenant resource serialized as JSON string. */
506
+ resource: {
507
+ type: "string",
508
+ required: true
509
+ },
510
+ /**
511
+ * Summary projection (key display fields as JSON string: id, displayName, status).
512
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
513
+ */
514
+ summary: {
515
+ type: "string",
516
+ required: true
517
+ },
518
+ /** Version id (e.g. ULID). */
519
+ vid: {
520
+ type: "string",
521
+ required: true
522
+ },
523
+ lastUpdated: {
524
+ type: "string",
525
+ required: true
526
+ },
527
+ gsi1Shard: gsi1ShardAttribute,
528
+ deleted: {
529
+ type: "boolean",
530
+ required: false
531
+ },
532
+ bundleId: {
533
+ type: "string",
534
+ required: false
535
+ },
536
+ msgId: {
537
+ type: "string",
538
+ required: false
539
+ }
540
+ },
541
+ indexes: {
542
+ /** Base table: PK = TENANT#ID#<tenantId>, SK = CURRENT. Do not supply PK or SK from outside. */
543
+ record: {
544
+ pk: {
545
+ field: "PK",
546
+ composite: ["tenantId"],
547
+ template: "TENANT#ID#${tenantId}"
548
+ },
549
+ sk: {
550
+ field: "SK",
551
+ composite: ["sk"],
552
+ template: "${sk}"
553
+ }
554
+ },
555
+ /**
556
+ * GSI1 — Unified Sharded List per ADR-011: list all Tenants across the four shards.
557
+ * Tenant lives at the platform tier (no parent tenant or workspace), so `TID#-#WID#-`
558
+ * sentinels precede `RT#Tenant#SHARD#<n>`. SK is `<ISO-8601 lastUpdated>#<id>` (control-plane
559
+ * unlabeled per DR-004). `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
560
+ */
561
+ gsi1: {
562
+ index: "GSI1",
563
+ pk: {
564
+ field: "GSI1PK",
565
+ composite: ["gsi1Shard"],
566
+ template: "TID#-#WID#-#RT#Tenant#SHARD#${gsi1Shard}"
567
+ },
568
+ sk: {
569
+ field: "GSI1SK",
570
+ casing: "none",
571
+ composite: ["lastUpdated", "id"],
572
+ template: "${lastUpdated}#${id}"
573
+ }
574
+ }
575
+ }
576
+ });
577
+
578
+ // src/data/dynamo/entities/control/user-entity.ts
579
+ var import_electrodb6 = require("electrodb");
580
+ var UserEntity = new import_electrodb6.Entity({
581
+ model: {
582
+ entity: "user",
583
+ service: "control",
584
+ version: "01"
585
+ },
586
+ attributes: {
587
+ /** Sort key sentinel. Always "CURRENT". */
588
+ sk: {
589
+ type: "string",
590
+ required: true,
591
+ default: "CURRENT"
592
+ },
593
+ /** FHIR Resource.id; platform user id (ohi_uid). */
594
+ id: {
595
+ type: "string",
596
+ required: true
597
+ },
598
+ /** Full User resource serialized as JSON string. */
599
+ resource: {
600
+ type: "string",
601
+ required: true
602
+ },
603
+ /**
604
+ * Summary projection (key display fields as JSON string: id, displayName, status).
605
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
606
+ */
607
+ summary: {
608
+ type: "string",
609
+ required: true
610
+ },
611
+ /**
612
+ * Immutable Cognito-issued `sub` claim. Drives GSI2 (sub-lookup). Optional until the
613
+ * Post Confirmation Lambda (#770) lands; required thereafter.
614
+ */
615
+ cognitoSub: {
616
+ type: "string",
617
+ required: false
618
+ },
619
+ /** Version id (e.g. ULID). */
620
+ vid: {
621
+ type: "string",
622
+ required: true
623
+ },
624
+ lastUpdated: {
625
+ type: "string",
626
+ required: true
627
+ },
628
+ gsi1Shard: gsi1ShardAttribute,
629
+ deleted: {
630
+ type: "boolean",
631
+ required: false
632
+ },
633
+ bundleId: {
634
+ type: "string",
635
+ required: false
636
+ },
637
+ msgId: {
638
+ type: "string",
639
+ required: false
640
+ }
641
+ },
642
+ indexes: {
643
+ /** Base table: PK = USER#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
644
+ record: {
645
+ pk: {
646
+ field: "PK",
647
+ composite: ["id"],
648
+ template: "USER#ID#${id}"
649
+ },
650
+ sk: {
651
+ field: "SK",
652
+ composite: ["sk"],
653
+ template: "${sk}"
654
+ }
655
+ },
656
+ /**
657
+ * GSI1 — Unified Sharded List per ADR-011: list all Users across the four shards.
658
+ * Non-tenant-isolated, so `TID#-#WID#-` sentinels precede `RT#User#SHARD#<n>`.
659
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
660
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z` characters.
661
+ */
662
+ gsi1: {
663
+ index: "GSI1",
664
+ pk: {
665
+ field: "GSI1PK",
666
+ composite: ["gsi1Shard"],
667
+ template: "TID#-#WID#-#RT#User#SHARD#${gsi1Shard}"
668
+ },
669
+ sk: {
670
+ field: "GSI1SK",
671
+ casing: "none",
672
+ composite: ["lastUpdated", "id"],
673
+ template: "${lastUpdated}#${id}"
674
+ }
675
+ },
676
+ /**
677
+ * GSI2 — Cognito sub-lookup per ADR-011: resolves the UserEntity from a Cognito `sub` claim.
678
+ * `condition` skips the index when `cognitoSub` is missing so legacy items without a sub are
679
+ * not indexed.
680
+ */
681
+ gsi2: {
682
+ index: "GSI2",
683
+ condition: (attrs) => typeof attrs.cognitoSub === "string" && attrs.cognitoSub.length > 0,
684
+ pk: {
685
+ field: "GSI2PK",
686
+ casing: "none",
687
+ composite: ["cognitoSub"],
688
+ template: "USER#SUB#${cognitoSub}"
689
+ },
690
+ sk: {
691
+ field: "GSI2SK",
692
+ casing: "none",
693
+ composite: [],
694
+ template: "CURRENT"
695
+ }
696
+ }
697
+ }
698
+ });
699
+
700
+ // src/data/dynamo/entities/control/workspace-entity.ts
701
+ var import_electrodb7 = require("electrodb");
702
+ var WorkspaceEntity = new import_electrodb7.Entity({
703
+ model: {
704
+ entity: "workspace",
705
+ service: "control",
706
+ version: "01"
707
+ },
708
+ attributes: {
709
+ /** Sort key sentinel. Always "CURRENT". */
710
+ sk: {
711
+ type: "string",
712
+ required: true,
713
+ default: "CURRENT"
714
+ },
715
+ /** Tenant that contains this workspace (required). */
716
+ tenantId: {
717
+ type: "string",
718
+ required: true
719
+ },
720
+ /** FHIR Resource.id; logical id in URL. */
721
+ id: {
722
+ type: "string",
723
+ required: true
724
+ },
725
+ /** Full Workspace resource serialized as JSON string. */
726
+ resource: {
727
+ type: "string",
728
+ required: true
729
+ },
730
+ /**
731
+ * Summary projection (key display fields as JSON string: id, displayName, status).
732
+ * Populated on every write via extractSummary(resource); GSI1 INCLUDE surfaces it on lists.
733
+ */
734
+ summary: {
735
+ type: "string",
736
+ required: true
737
+ },
738
+ /** Version id (e.g. ULID). */
739
+ vid: {
740
+ type: "string",
741
+ required: true
742
+ },
743
+ lastUpdated: {
744
+ type: "string",
745
+ required: true
746
+ },
747
+ gsi1Shard: gsi1ShardAttribute,
748
+ deleted: {
749
+ type: "boolean",
750
+ required: false
751
+ },
752
+ bundleId: {
753
+ type: "string",
754
+ required: false
755
+ },
756
+ msgId: {
757
+ type: "string",
758
+ required: false
759
+ }
760
+ },
761
+ indexes: {
762
+ /** Base table: PK = TID#<tenantId>#WORKSPACE#ID#<id>, SK = CURRENT. Do not supply PK or SK from outside. */
763
+ record: {
764
+ pk: {
765
+ field: "PK",
766
+ composite: ["tenantId", "id"],
767
+ template: "TID#${tenantId}#WORKSPACE#ID#${id}"
768
+ },
769
+ sk: {
770
+ field: "SK",
771
+ composite: ["sk"],
772
+ template: "${sk}"
773
+ }
774
+ },
775
+ /**
776
+ * GSI1 — Unified Sharded List per ADR-011: list all Workspaces for a tenant across the
777
+ * four shards. Workspace is itself the workspace identity, so `WID#-` is a sentinel.
778
+ * SK is `<ISO-8601 lastUpdated>#<id>` (control-plane unlabeled per DR-004).
779
+ * `casing: "none"` on the SK preserves ISO-8601 `T`/`Z`.
780
+ */
781
+ gsi1: {
782
+ index: "GSI1",
783
+ pk: {
784
+ field: "GSI1PK",
785
+ composite: ["tenantId", "gsi1Shard"],
786
+ template: "TID#${tenantId}#WID#-#RT#Workspace#SHARD#${gsi1Shard}"
787
+ },
788
+ sk: {
789
+ field: "GSI1SK",
790
+ casing: "none",
791
+ composite: ["lastUpdated", "id"],
792
+ template: "${lastUpdated}#${id}"
793
+ }
794
+ }
795
+ }
796
+ });
797
+
798
+ // src/data/dynamo/dynamo-control-service.ts
799
+ var controlPlaneEntities = {
800
+ configuration: ConfigurationEntity,
801
+ membership: MembershipEntity,
802
+ role: RoleEntity,
803
+ roleAssignment: RoleAssignmentEntity,
804
+ tenant: TenantEntity,
805
+ user: UserEntity,
806
+ workspace: WorkspaceEntity
807
+ };
808
+ var controlPlaneService = new import_electrodb8.Service(controlPlaneEntities, {
809
+ table: defaultTableName,
810
+ client: dynamoClient
811
+ });
812
+ var DynamoControlService = {
813
+ entities: controlPlaneService.entities
31
814
  };
815
+ function getDynamoControlService(tableName) {
816
+ const resolved = tableName ?? defaultTableName;
817
+ const service = new import_electrodb8.Service(controlPlaneEntities, {
818
+ table: resolved,
819
+ client: dynamoClient
820
+ });
821
+ return {
822
+ entities: service.entities
823
+ };
824
+ }
825
+
826
+ // src/components/cognito/pre-token-generation.handler.ts
827
+ var REFERENCE_TYPES = {
828
+ Tenant: "Tenant/",
829
+ Workspace: "Workspace/"
830
+ };
831
+ function idFromReference(reference, prefix) {
832
+ if (!reference || !reference.startsWith(prefix)) return void 0;
833
+ const id = reference.slice(prefix.length);
834
+ return id.length > 0 ? id : void 0;
835
+ }
836
+ function displayNameFor(user) {
837
+ const first = user.name?.[0];
838
+ return first?.text ?? first?.family ?? user.displayName ?? void 0;
839
+ }
840
+ function safeParseUser(json) {
841
+ try {
842
+ return JSON.parse(json);
843
+ } catch {
844
+ return void 0;
845
+ }
846
+ }
847
+ async function findUserBySub(service, cognitoSub) {
848
+ const result = await service.entities.user.query.gsi2({ cognitoSub }).go({ limit: 1 });
849
+ const item = result.data?.[0];
850
+ if (!item) return void 0;
851
+ return {
852
+ id: item.id,
853
+ cognitoSub: item.cognitoSub,
854
+ resource: item.resource,
855
+ vid: item.vid
856
+ };
857
+ }
858
+ async function resolveClaims(cognitoSub) {
859
+ const service = getDynamoControlService();
860
+ const user = await findUserBySub(service, cognitoSub);
861
+ if (!user) {
862
+ console.warn(
863
+ `PreTokenGeneration: no User found for cognitoSub; emitting token without OpenHI claims (sub=${cognitoSub})`
864
+ );
865
+ return void 0;
866
+ }
867
+ const parsed = safeParseUser(user.resource);
868
+ if (!parsed) {
869
+ console.warn(
870
+ `PreTokenGeneration: User resource JSON could not be parsed (sub=${cognitoSub}, id=${user.id})`
871
+ );
872
+ return void 0;
873
+ }
874
+ const tenantId = idFromReference(
875
+ parsed.currentTenant?.reference,
876
+ REFERENCE_TYPES.Tenant
877
+ );
878
+ const workspaceId = idFromReference(
879
+ parsed.currentWorkspace?.reference,
880
+ REFERENCE_TYPES.Workspace
881
+ );
882
+ const displayName = displayNameFor(parsed);
883
+ if (!tenantId || !workspaceId || !displayName) {
884
+ console.warn(
885
+ `PreTokenGeneration: resolved User missing currentTenant/currentWorkspace/displayName; emitting token without OpenHI claims (sub=${cognitoSub}, id=${user.id})`
886
+ );
887
+ return void 0;
888
+ }
889
+ return {
890
+ ohi_tid: tenantId,
891
+ ohi_wid: workspaceId,
892
+ ohi_uid: user.id,
893
+ ohi_uname: displayName
894
+ };
895
+ }
32
896
  var handler = async (event, _context) => {
33
- console.debug(`Raw event=${event}`);
34
897
  try {
898
+ const cognitoSub = event.request?.userAttributes?.sub;
899
+ if (!cognitoSub) {
900
+ console.warn(
901
+ "PreTokenGeneration: event has no Cognito sub; returning event unchanged"
902
+ );
903
+ return event;
904
+ }
905
+ const claims = await resolveClaims(cognitoSub);
906
+ if (!claims) return event;
35
907
  if (!event.response) {
36
908
  event.response = {};
37
909
  }
38
910
  const response = event.response;
39
- const claimsToAdd = { ...OPENHI_CLAIMS };
40
911
  response.claimsAndScopeOverrideDetails = {
41
912
  accessTokenGeneration: {
42
- claimsToAddOrOverride: claimsToAdd
913
+ claimsToAddOrOverride: { ...claims }
43
914
  },
44
915
  idTokenGeneration: {
45
- claimsToAddOrOverride: claimsToAdd
916
+ claimsToAddOrOverride: { ...claims }
46
917
  }
47
918
  };
48
- } catch {
49
- console.warn("Event is missing tenant or workspace ID...");
919
+ } catch (err) {
920
+ console.warn(
921
+ "PreTokenGeneration: unexpected error; returning event unchanged",
922
+ err
923
+ );
50
924
  }
51
925
  return event;
52
926
  };