@openhi/constructs 0.0.104 → 0.0.105

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