@openhi/constructs 0.0.151 → 0.0.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/{chunk-AWYZJFPL.mjs → chunk-CFJDATDK.mjs} +8 -1
- package/lib/chunk-CFJDATDK.mjs.map +1 -0
- package/lib/{chunk-WGA43MMY.mjs → chunk-SXYY5WHG.mjs} +194 -71
- package/lib/chunk-SXYY5WHG.mjs.map +1 -0
- package/lib/{events-CMG8xanm.d.mts → events-DTgo2dcW.d.mts} +2 -14
- package/lib/{events-CMG8xanm.d.ts → events-DTgo2dcW.d.ts} +2 -14
- package/lib/index.d.mts +54 -7
- package/lib/index.d.ts +53 -18
- package/lib/index.js +53 -19
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +35 -4
- package/lib/index.mjs.map +1 -1
- package/lib/provision-default-workspace.handler.js +6 -0
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +1 -1
- package/lib/rest-api-lambda.handler.js +6 -0
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +1 -1
- package/lib/seed-demo-data.handler.d.mts +14 -1
- package/lib/seed-demo-data.handler.d.ts +14 -1
- package/lib/seed-demo-data.handler.js +199 -68
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +2 -2
- package/package.json +5 -5
- package/lib/chunk-AWYZJFPL.mjs.map +0 -1
- package/lib/chunk-WGA43MMY.mjs.map +0 -1
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
getDynamoControlService
|
|
23
23
|
} from "./chunk-VZCPGQXA.mjs";
|
|
24
24
|
|
|
25
|
+
// src/data/operations/control/membership-constraints/platform-scope-tenant-id.ts
|
|
26
|
+
var PLATFORM_SCOPE_TENANT_ID = "platform";
|
|
27
|
+
|
|
25
28
|
// src/data/operations/control/membership/membership-create-operation.ts
|
|
26
29
|
import {
|
|
27
30
|
assertLinkedDataIdentityCardinality,
|
|
@@ -170,6 +173,9 @@ import { extractSummary as extractSummary2 } from "@openhi/types";
|
|
|
170
173
|
var TENANT_LANE_SK_PREFIX = "MEMBERSHIP#TENANT#";
|
|
171
174
|
async function assertUserHasTenantMembershipOperation(params) {
|
|
172
175
|
const { userId, tenantId, tableName } = params;
|
|
176
|
+
if (tenantId === PLATFORM_SCOPE_TENANT_ID) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
173
179
|
const service = getDynamoControlService(tableName);
|
|
174
180
|
const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: TENANT_LANE_SK_PREFIX }).go();
|
|
175
181
|
const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);
|
|
@@ -380,10 +386,11 @@ async function createWorkspaceOperation(params) {
|
|
|
380
386
|
}
|
|
381
387
|
|
|
382
388
|
export {
|
|
389
|
+
PLATFORM_SCOPE_TENANT_ID,
|
|
383
390
|
extractDenormalizedReferenceDisplay,
|
|
384
391
|
createMembershipOperation,
|
|
385
392
|
createRoleAssignmentOperation,
|
|
386
393
|
createTenantOperation,
|
|
387
394
|
createWorkspaceOperation
|
|
388
395
|
};
|
|
389
|
-
//# sourceMappingURL=chunk-
|
|
396
|
+
//# sourceMappingURL=chunk-CFJDATDK.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/control/membership-constraints/platform-scope-tenant-id.ts","../src/data/operations/control/membership/membership-create-operation.ts","../src/data/operations/control/denormalized-display-names.ts","../src/data/operations/control/membership-constraints/assert-workspace-in-tenant-operation.ts","../src/data/operations/control/roleassignment/roleassignment-create-operation.ts","../src/data/operations/control/membership-constraints/assert-user-has-tenant-membership-operation.ts","../src/data/operations/control/tenant/tenant-create-operation.ts","../src/data/operations/control/workspace/workspace-create-operation.ts","../src/data/operations/data/organization/organization-provision-for-workspace-operation.ts"],"sourcesContent":["/**\n * Sentinel `tenantId` used on platform-scoped `RoleAssignment` records.\n * A platform-scoped RA (e.g. `system-admin`) spans every tenant, but\n * the `RoleAssignment` entity requires a `tenantId` on its key for\n * sharding — there is no real tenant to point at. The `\"platform\"`\n * literal is a reserved value that never matches a real `Tenant.id`\n * and signals \"this RA scopes across all tenants\".\n *\n * The constant lives in the data layer so both the\n * {@link assertUserHasTenantMembershipOperation} write-time constraint\n * and the seed-demo-data workflow can read it without the constraint\n * helper having to reach back into the workflow package — which would\n * create a backwards data-layer-to-workflow dependency. The\n * `workflows/control-plane/seed-demo-data/events.ts` module re-exports\n * the constant under the same name for back-compat with existing\n * imports that point at the workflow's barrel.\n *\n * Renaming this constant is a wire-format break — the seed-demo-data\n * handler emits `RoleAssignment` records keyed on this value, and the\n * in-band records written under it become unreachable if the sentinel\n * changes.\n */\nexport const PLATFORM_SCOPE_TENANT_ID = \"platform\";\n","import {\n assertLinkedDataIdentityCardinality,\n extractSummary,\n type Extension,\n type FhirResourceLike,\n LinkedDataIdentityCardinalityError,\n} from \"@openhi/types\";\nimport {\n buildMembershipUserProjectionItem,\n extractReferenceSlug,\n} from \"./membership-user-projection\";\nimport { buildMembershipWorkspaceProjectionItem } from \"./membership-workspace-projection\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ValidationError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport { extractDenormalizedReferenceDisplay } from \"../denormalized-display-names\";\nimport { assertWorkspaceInTenantOperation } from \"../membership-constraints/assert-workspace-in-tenant-operation\";\nimport {\n executeMultiWrite,\n type MultiWriteTriple,\n} from \"../multi-write-operation\";\n\nexport interface MembershipCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface MembershipCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createMembershipOperation(\n params: MembershipCreateParams,\n): Promise<MembershipCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `membership-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"Membership\", id, ...parsedResource };\n\n let linkedDataIdentityRef: string | undefined;\n try {\n const ext = assertLinkedDataIdentityCardinality(\n resource as { extension?: Array<Extension> },\n );\n linkedDataIdentityRef = ext?.valueReference?.reference;\n } catch (e) {\n if (e instanceof LinkedDataIdentityCardinalityError) {\n throw new ValidationError(e.message, { cause: e });\n }\n throw e;\n }\n\n // TR-024 denormalized display-name attributes. The authoritative\n // write-time source per TR-024 rule 2 is the canonical Tenant / User\n // record's `displayName`. Until the carrier-record-lookup pass lands\n // (#1010 follow-up), the foundational fallback (#1009) reads the\n // FHIR Reference.display values supplied on the resource so the\n // adjacency-list projection writer here can compose SKs from\n // top-level attributes.\n const resourceRecord = resource as Record<string, unknown>;\n const denormalizedTenantName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"tenant\",\n );\n const denormalizedUserName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"user\",\n );\n const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"workspace\",\n );\n\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n // ADR-018 patterns #2 / #3 / #4 — user- and workspace-partition\n // projection rows. The projection's discriminating fields come from\n // the same FHIR Reference shape the canonical row reads, so the\n // canonical and projection writes always agree on the underlying\n // user / workspace identifiers. Missing identifiers (no user\n // reference at all) skip the user-projection; tenant-scoped\n // Memberships (no workspaceId) skip the workspace-projection — the\n // canonical row still lands either way.\n const userIdFromResource = extractReferenceSlug(resourceRecord, \"user\");\n const workspaceIdFromResource = extractReferenceSlug(\n resourceRecord,\n \"workspace\",\n );\n\n // ADR 2026-03-13-02 § 2 Consequences/Negative — when the resource\n // names a workspace, that workspace must belong to the referenced\n // tenant. Prefer the resource's `tenant.reference` (the issue's\n // \"referenced tenant\" wording) and fall back to the context tenant\n // when the body omits the reference. The check fires before any\n // write so the constraint is enforced atomically with respect to\n // the canonical row.\n if (workspaceIdFromResource !== undefined) {\n const tenantIdFromResource = extractReferenceSlug(resourceRecord, \"tenant\");\n const referencedTenantId = tenantIdFromResource ?? context.tenantId;\n await assertWorkspaceInTenantOperation({\n tenantId: referencedTenantId,\n workspaceId: workspaceIdFromResource,\n tableName,\n });\n }\n\n const userProjectionItem =\n userIdFromResource !== undefined\n ? buildMembershipUserProjectionItem({\n tenantId: context.tenantId,\n userId: userIdFromResource,\n workspaceId: workspaceIdFromResource,\n membershipId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedWorkspaceName,\n })\n : undefined;\n\n const workspaceProjectionItem =\n userIdFromResource !== undefined && workspaceIdFromResource !== undefined\n ? buildMembershipWorkspaceProjectionItem({\n tenantId: context.tenantId,\n workspaceId: workspaceIdFromResource,\n userId: userIdFromResource,\n membershipId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedUserName,\n })\n : undefined;\n\n const canonicalItem = {\n tenantId: context.tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n linkedDataIdentityRef,\n denormalizedTenantName,\n denormalizedUserName,\n };\n\n const triples: Array<MultiWriteTriple> = [\n { entity: \"membership\", action: \"put\", item: canonicalItem },\n ];\n if (userProjectionItem) {\n triples.push({\n entity: \"membershipUserProjection\",\n action: \"put\",\n item: userProjectionItem as unknown as Record<string, unknown>,\n });\n }\n if (workspaceProjectionItem) {\n triples.push({\n entity: \"membershipWorkspaceProjection\",\n action: \"put\",\n item: workspaceProjectionItem as unknown as Record<string, unknown>,\n });\n }\n\n await executeMultiWrite({ service, triples });\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","/**\n * Helpers that capture denormalized display-name attributes from an\n * incoming FHIR Reference on Membership and RoleAssignment resources.\n *\n * **Foundational implementation for TR-024.** ADR-018 § Implementation\n * Notes and TR-024 § Recommendation pin the authoritative write-time\n * source as the carrier entity's canonical `displayName` — i.e. the\n * operations layer reads the canonical Tenant / User / Role record\n * by id and copies its display name into the relationship's\n * `denormalized<CarrierEntity>Name` field on the same\n * `TransactWriteItems`. That contract is owned by the operations-layer\n * multi-write helper filed as #1010.\n *\n * This module is the **foundational fallback** that issue #1009 uses\n * before #1010 lands: it reads the display string a client already\n * supplies on the resource's `Reference.display` field. The fallback\n * keeps Membership / RoleAssignment write paths populating the new\n * top-level attributes today (so adjacency-list projection writers\n * downstream can rely on them) while letting #1010 own the canonical-\n * row lookup without re-plumbing the write paths.\n *\n * @see TR-024 — Denormalized display-name attributes\n * @see ADR-018 § Implementation Notes\n */\n\n/**\n * Returns the trimmed display string from `resource[fieldName].display`\n * when present and non-empty; otherwise returns `undefined`. Used by the\n * Membership and RoleAssignment create / update operations to populate\n * top-level `denormalized<CarrierEntity>Name` attributes from incoming\n * FHIR Reference fields.\n *\n * Field name maps to the FHIR field on the resource:\n * - `Membership.tenant` → `\"tenant\"`\n * - `Membership.user` → `\"user\"`\n * - `RoleAssignment.tenant` → `\"tenant\"`\n * - `RoleAssignment.user` → `\"user\"`\n * - `RoleAssignment.role` → `\"role\"`\n *\n * Guards against malformed payloads (non-object `field`, non-string\n * `display`, empty strings after trim) so a single bad write never\n * blocks an entity put — matching the same defensive posture\n * `gsi1skAttribute` takes for the `resource` JSON parse.\n */\nexport function extractDenormalizedReferenceDisplay(\n resource: Record<string, unknown>,\n fieldName: string,\n): string | undefined {\n const field = resource[fieldName];\n if (!field || typeof field !== \"object\") {\n return undefined;\n }\n const display = (field as { display?: unknown }).display;\n if (typeof display !== \"string\") {\n return undefined;\n }\n const trimmed = display.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ConflictError } from \"../../../errors\";\n\n/** Inputs to {@link assertWorkspaceInTenantOperation}. */\nexport interface AssertWorkspaceInTenantParams {\n /** Tenant the workspace must belong to. */\n readonly tenantId: string;\n /** Workspace id that must exist under `tenantId`. */\n readonly workspaceId: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Throws {@link ConflictError} when no `Workspace` with id `workspaceId`\n * exists under `tenantId`.\n *\n * Implementation: a single base-table `get` against the Workspace entity\n * (`PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK = CURRENT`). The\n * workspace's tenant is encoded into the PK by construction, so a hit\n * *is* the constraint check — there is no separate `managingOrganization`\n * comparison required.\n *\n * @see ADR 2026-03-13-02 § 2 — Consequences/Negative\n */\nexport async function assertWorkspaceInTenantOperation(\n params: AssertWorkspaceInTenantParams,\n): Promise<void> {\n const { tenantId, workspaceId, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const { data: item } = await service.entities.workspace\n .get({ tenantId, id: workspaceId, sk: \"CURRENT\" })\n .go();\n\n if (!item) {\n throw new ConflictError(\n `Workspace ${workspaceId} does not belong to tenant ${tenantId}; the workspace must be created in the referenced tenant before this resource can reference it.`,\n { details: { tenantId, workspaceId } },\n );\n }\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport {\n buildRoleAssignmentUserProjectionItem,\n extractReferenceSlug,\n} from \"./roleassignment-user-projection\";\nimport { buildRoleAssignmentWorkspaceProjectionItem } from \"./roleassignment-workspace-projection\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport { extractDenormalizedReferenceDisplay } from \"../denormalized-display-names\";\nimport { assertUserHasTenantMembershipOperation } from \"../membership-constraints/assert-user-has-tenant-membership-operation\";\nimport { assertWorkspaceInTenantOperation } from \"../membership-constraints/assert-workspace-in-tenant-operation\";\nimport {\n executeMultiWrite,\n type MultiWriteTriple,\n} from \"../multi-write-operation\";\n\nexport interface RoleAssignmentCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface RoleAssignmentCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createRoleAssignmentOperation(\n params: RoleAssignmentCreateParams,\n): Promise<RoleAssignmentCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `roleassignment-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"RoleAssignment\", id, ...parsedResource };\n\n // TR-024 denormalized display-name attributes. The authoritative\n // write-time source per TR-024 rule 2 is the canonical Tenant / User /\n // Role record's `displayName`. Until the carrier-record-lookup pass\n // lands (#1010 follow-up), the foundational fallback (#1009) reads\n // the FHIR Reference.display values supplied on the resource so the\n // adjacency-list projection writer here can compose SKs from\n // top-level attributes.\n const resourceRecord = resource as Record<string, unknown>;\n const denormalizedTenantName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"tenant\",\n );\n const denormalizedUserName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"user\",\n );\n const denormalizedRoleName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"role\",\n );\n\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n // ADR-018 patterns #5 / #9 — user- and workspace-partition projection\n // rows. The projection's discriminating fields come from the same\n // FHIR Reference shape the canonical row reads, so the canonical and\n // projection writes always agree on the underlying user / role /\n // workspace identifiers. Missing user or role references skip the\n // user-projection; tenant-scoped RoleAssignments (no workspaceId)\n // skip the workspace-projection — the canonical row still lands\n // either way.\n const userIdFromResource = extractReferenceSlug(resourceRecord, \"user\");\n const roleIdFromResource = extractReferenceSlug(resourceRecord, \"role\");\n const workspaceIdFromResource = extractReferenceSlug(\n resourceRecord,\n \"workspace\",\n );\n\n // ADR 2026-03-13-02 § 2 Consequences/Negative — a RoleAssignment may\n // only be created when the referenced user already holds a\n // tenant-level Membership in the referenced tenant, and (when\n // workspace-scoped) that workspace must belong to the same tenant.\n // Prefer the resource's `tenant.reference` (the issue's \"referenced\n // tenant\" wording) and fall back to the context tenant when the\n // body omits the reference. Constraints fire before any write so\n // the rejection is atomic with respect to the canonical row.\n if (userIdFromResource !== undefined) {\n const tenantIdFromResource = extractReferenceSlug(resourceRecord, \"tenant\");\n const referencedTenantId = tenantIdFromResource ?? context.tenantId;\n await assertUserHasTenantMembershipOperation({\n userId: userIdFromResource,\n tenantId: referencedTenantId,\n tableName,\n });\n if (workspaceIdFromResource !== undefined) {\n await assertWorkspaceInTenantOperation({\n tenantId: referencedTenantId,\n workspaceId: workspaceIdFromResource,\n tableName,\n });\n }\n }\n\n const userProjectionItem =\n userIdFromResource !== undefined && roleIdFromResource !== undefined\n ? buildRoleAssignmentUserProjectionItem({\n tenantId: context.tenantId,\n userId: userIdFromResource,\n workspaceId: workspaceIdFromResource,\n roleId: roleIdFromResource,\n roleAssignmentId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedRoleName,\n })\n : undefined;\n\n const workspaceProjectionItem =\n userIdFromResource !== undefined &&\n roleIdFromResource !== undefined &&\n workspaceIdFromResource !== undefined\n ? buildRoleAssignmentWorkspaceProjectionItem({\n tenantId: context.tenantId,\n workspaceId: workspaceIdFromResource,\n userId: userIdFromResource,\n roleId: roleIdFromResource,\n roleAssignmentId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedUserName,\n denormalizedRoleName,\n })\n : undefined;\n\n const canonicalItem = {\n tenantId: context.tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedRoleName,\n };\n\n const triples: Array<MultiWriteTriple> = [\n { entity: \"roleAssignment\", action: \"put\", item: canonicalItem },\n ];\n if (userProjectionItem) {\n triples.push({\n entity: \"roleAssignmentUserProjection\",\n action: \"put\",\n item: userProjectionItem as unknown as Record<string, unknown>,\n });\n }\n if (workspaceProjectionItem) {\n triples.push({\n entity: \"roleAssignmentWorkspaceProjection\",\n action: \"put\",\n item: workspaceProjectionItem as unknown as Record<string, unknown>,\n });\n }\n\n await executeMultiWrite({ service, triples });\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { PLATFORM_SCOPE_TENANT_ID } from \"./platform-scope-tenant-id\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ConflictError } from \"../../../errors\";\n\n/**\n * SK prefix for the tenant-level Membership sub-lane (ADR-018 pattern #3).\n *\n * Tenant-level Memberships project under the user partition with\n * `SK = MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<membershipId>`.\n * Because the `<tenantId>` segment appears *after* `<normalizedTenantName>`,\n * a `begins_with` filter cannot narrow to a single tenant — we read every\n * tenant-lane row for the user and filter on the projection-row's\n * `tenantId` attribute in memory.\n */\nconst TENANT_LANE_SK_PREFIX = \"MEMBERSHIP#TENANT#\";\n\n/** Inputs to {@link assertUserHasTenantMembershipOperation}. */\nexport interface AssertUserHasTenantMembershipParams {\n /** The user that must already be a member of `tenantId`. */\n readonly userId: string;\n /** The tenant the user must hold a tenant-level Membership in. */\n readonly tenantId: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Throws {@link ConflictError} when `userId` has no tenant-level\n * `Membership` in `tenantId`.\n *\n * Implementation: queries the ADR-018 user-partition projection\n * (`MembershipUserProjectionEntity`) under\n * `PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#TENANT#'` — a single\n * strongly-consistent base-table query with no GSI hop — and filters the\n * returned rows for `tenantId` in memory.\n *\n * The filter cannot be expressed as an additional `begins_with` because\n * the projection SK encodes `<normalizedTenantName>` *before* `<tenantId>`\n * (`MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<id>`). The\n * scan cost is bounded by the user's tenant-membership fan-out, which is\n * small by construction.\n *\n * @see ADR 2026-03-13-02 § 2 — Consequences/Negative\n * @see ADR-018 § Access Pattern Coverage (pattern #3)\n */\nexport async function assertUserHasTenantMembershipOperation(\n params: AssertUserHasTenantMembershipParams,\n): Promise<void> {\n const { userId, tenantId, tableName } = params;\n\n // The platform-scope sentinel is not a real tenant — it is a\n // reserved `tenantId` value used on platform-scoped RoleAssignments\n // (e.g. `system-admin`). No `Membership` is ever created under it,\n // and a Membership there would have no semantic meaning. Skip the\n // tenant-membership constraint so platform-scoped RAs can be\n // created without requiring an impossible-to-create prerequisite\n // Membership row.\n if (tenantId === PLATFORM_SCOPE_TENANT_ID) {\n return;\n }\n\n const service = getDynamoControlService(tableName);\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: TENANT_LANE_SK_PREFIX })\n .go();\n\n const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);\n if (!matched) {\n throw new ConflictError(\n `User ${userId} has no tenant-level Membership in tenant ${tenantId}; a Membership must exist before a RoleAssignment can be created.`,\n { details: { userId, tenantId } },\n );\n }\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface CreateTenantParams {\n context: OpenHiContext;\n body: {\n id?: string;\n resource?: Record<string, unknown> | string;\n };\n tableName?: string;\n}\n\nexport interface CreateTenantResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Creates a Tenant. Generates an id if not provided.\n */\nexport async function createTenantOperation(\n params: CreateTenantParams,\n): Promise<CreateTenantResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `tenant-${Date.now()}`;\n const lastUpdated = context.date;\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const resource = { resourceType: \"Tenant\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.tenant\n .put({\n tenantId: id,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return { id, resource, meta: { lastUpdated, versionId: vid } };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport { provisionOrganizationForWorkspaceOperation } from \"../../data/organization/organization-provision-for-workspace-operation\";\n\nexport interface CreateWorkspaceParams {\n context: OpenHiContext;\n body: {\n id?: string;\n resource?: Record<string, unknown> | string;\n };\n tableName?: string;\n}\n\nexport interface CreateWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Creates a Workspace scoped to the context tenant. Generates an id if not provided.\n */\nexport async function createWorkspaceOperation(\n params: CreateWorkspaceParams,\n): Promise<CreateWorkspaceResult> {\n const { context, body, tableName } = params;\n const { tenantId } = context;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `workspace-${Date.now()}`;\n const lastUpdated = context.date;\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const resource = { resourceType: \"Workspace\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.workspace\n .put({\n tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n const workspaceName =\n typeof parsedResource.name === \"string\" ? parsedResource.name : undefined;\n await provisionOrganizationForWorkspaceOperation({\n context,\n workspaceId: id,\n workspaceName,\n tableName,\n });\n\n return { id, resource, meta: { lastUpdated, versionId: vid } };\n}\n","import type { Organization, Reference } from \"@openhi/types\";\nimport { getDynamoDataService } from \"../../../dynamo/dynamo-data-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n createDataEntityRecord,\n DATA_ENTITY_SK,\n type SingleResourceResult,\n} from \"../../data-operations-common\";\n\n/**\n * Provision the slim, id-share Organization that represents a Workspace on the data plane.\n *\n * - `Organization.id === workspaceId` (id-share with the Workspace).\n * - `name` is populated from the Workspace's name when provided.\n * - `partOf` is populated when the Workspace's Tenant has an Organization (id-share at the\n * tenant level: `Tenant.id === TenantOrganization.id`). Tenant-level provisioning is out of\n * scope for #1001 — the lookup returns `undefined` until Tenant Organizations exist.\n *\n * Idempotent: re-running with the same params overwrites the same PK with the same payload.\n *\n * @see https://github.com/codedrifters/openhi/issues/1001\n */\nexport interface ProvisionOrganizationForWorkspaceParams {\n context: OpenHiContext;\n /** Workspace id; the provisioned Organization's id equals this value. */\n workspaceId: string;\n /** Name to record on the Organization. Derived from the Workspace's `name` by the caller. */\n workspaceName?: string;\n /** Optional table name override; resolved by data service from DYNAMO_TABLE_NAME when omitted. */\n tableName?: string;\n}\n\nexport type ProvisionOrganizationForWorkspaceResult =\n SingleResourceResult<Organization>;\n\nexport async function provisionOrganizationForWorkspaceOperation(\n params: ProvisionOrganizationForWorkspaceParams,\n): Promise<ProvisionOrganizationForWorkspaceResult> {\n const { context, workspaceId, workspaceName, tableName } = params;\n const { tenantId, date } = context;\n const service = getDynamoDataService(tableName);\n\n const partOf = await resolveTenantOrganizationReference(service, tenantId);\n\n const resource: Organization = {\n resourceType: \"Organization\",\n id: workspaceId,\n ...(workspaceName !== undefined && workspaceName !== \"\"\n ? { name: workspaceName }\n : {}),\n ...(partOf !== undefined ? { partOf } : {}),\n meta: {\n lastUpdated: date,\n versionId: \"1\",\n },\n };\n\n return createDataEntityRecord<Organization>(\n service.entities.organization as Parameters<\n typeof createDataEntityRecord\n >[0],\n tenantId,\n workspaceId,\n workspaceId,\n resource,\n date,\n );\n}\n\n/**\n * Resolve the Tenant's Organization reference for `Organization.partOf`. Returns `undefined`\n * when the Tenant has no Organization yet (the default until Tenant Organization provisioning\n * is wired up in a follow-up).\n *\n * Lookup convention: the Tenant's Organization is stored at\n * `(tenantId, workspaceId=tenantId, id=tenantId)` — a self-scope key that mirrors the\n * Workspace id-share pattern one level up. This scoping convention will be ratified by the\n * Tenant Organization provisioning issue; the OrganizationEntity comment notes strict\n * isolation requires both `tenantId` and `workspaceId`, so the self-scope is the\n * simplest deterministic location.\n */\nasync function resolveTenantOrganizationReference(\n service: ReturnType<typeof getDynamoDataService>,\n tenantId: string,\n): Promise<Reference | undefined> {\n const result = await (\n service.entities.organization as {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: unknown }> };\n }\n )\n .get({\n tenantId,\n workspaceId: tenantId,\n id: tenantId,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (result.data === null || result.data === undefined) {\n return undefined;\n }\n\n return { reference: `Organization/${tenantId}` };\n}\n"],"mappings":"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","names":["extractSummary","extractSummary","extractReferenceSlug","extractSummary","extractSummary","extractSummary","extractSummary"]}
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
require_lib
|
|
11
11
|
} from "./chunk-ZM4GDHHC.mjs";
|
|
12
12
|
import {
|
|
13
|
+
PLATFORM_SCOPE_TENANT_ID,
|
|
13
14
|
createMembershipOperation,
|
|
14
15
|
createRoleAssignmentOperation,
|
|
15
16
|
createTenantOperation,
|
|
16
17
|
createWorkspaceOperation
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CFJDATDK.mjs";
|
|
18
19
|
import {
|
|
19
20
|
NotFoundError
|
|
20
21
|
} from "./chunk-FYHBHHWK.mjs";
|
|
@@ -50,7 +51,6 @@ var SEED_DEMO_DATA_CONSUMER_NAME = "seed-demo-data";
|
|
|
50
51
|
var DEMO_URN_SYSTEM = "urn:openhi:demo";
|
|
51
52
|
var OPENHI_RESOURCE_URN_SYSTEM = "http://openhi.org/";
|
|
52
53
|
var DEMO_PERIOD = { start: "2026-01-01T00:00:00Z" };
|
|
53
|
-
var PLATFORM_SCOPE_TENANT_ID = "platform";
|
|
54
54
|
var PLACEHOLDER_TENANT_ID = "placeholder-tenant-id";
|
|
55
55
|
var PLACEHOLDER_WORKSPACE_ID = "placeholder-workspace-id";
|
|
56
56
|
var DEV_USERS = [
|
|
@@ -561,6 +561,25 @@ var errorMessage = (err) => {
|
|
|
561
561
|
}
|
|
562
562
|
return String(err);
|
|
563
563
|
};
|
|
564
|
+
var tryRun = async (failures, phase, scope, resourceType, resourceId, fn) => {
|
|
565
|
+
try {
|
|
566
|
+
await fn();
|
|
567
|
+
return true;
|
|
568
|
+
} catch (err) {
|
|
569
|
+
failures.push({ phase, scope, resourceType, resourceId, error: err });
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var aggregateFailureError = (failures) => {
|
|
574
|
+
const summary = failures.map(
|
|
575
|
+
(f) => `${f.phase} ${f.scope}/${f.resourceType}/${f.resourceId}: ${errorMessage(
|
|
576
|
+
f.error
|
|
577
|
+
)}`
|
|
578
|
+
).join("; ");
|
|
579
|
+
return new Error(
|
|
580
|
+
`seed-demo-data: ${failures.length} item(s) failed across phases: ${summary}`
|
|
581
|
+
);
|
|
582
|
+
};
|
|
564
583
|
var idForRoleCode = (code) => {
|
|
565
584
|
for (const key of Object.keys(PLATFORM_ROLE_IDS)) {
|
|
566
585
|
if (PLATFORM_ROLE_CONCEPTS[key].code === code) {
|
|
@@ -670,95 +689,180 @@ var upsertUser = async (context, user, cognitoSub) => {
|
|
|
670
689
|
lastUpdated: context.date ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
671
690
|
}).go();
|
|
672
691
|
};
|
|
673
|
-
var seedWorkspaceDataPlane = async (baseContext, group) => {
|
|
692
|
+
var seedWorkspaceDataPlane = async (baseContext, group, failures) => {
|
|
674
693
|
const workspaceContext = {
|
|
675
694
|
...baseContext,
|
|
676
695
|
tenantId: group.tenantId,
|
|
677
696
|
workspaceId: group.workspaceId
|
|
678
697
|
};
|
|
698
|
+
const scope = `${group.tenantId}/${group.workspaceId}`;
|
|
679
699
|
for (const patient of group.patients) {
|
|
680
|
-
await
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
700
|
+
await tryRun(
|
|
701
|
+
failures,
|
|
702
|
+
"phase-3",
|
|
703
|
+
scope,
|
|
704
|
+
"Patient",
|
|
705
|
+
patient.id ?? "",
|
|
706
|
+
() => createPatientOperation({
|
|
707
|
+
context: workspaceContext,
|
|
708
|
+
body: patient
|
|
709
|
+
})
|
|
710
|
+
);
|
|
684
711
|
}
|
|
685
712
|
for (const practitioner of group.practitioners) {
|
|
686
|
-
await
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
713
|
+
await tryRun(
|
|
714
|
+
failures,
|
|
715
|
+
"phase-3",
|
|
716
|
+
scope,
|
|
717
|
+
"Practitioner",
|
|
718
|
+
practitioner.id ?? "",
|
|
719
|
+
() => createPractitionerOperation({
|
|
720
|
+
context: workspaceContext,
|
|
721
|
+
body: practitioner
|
|
722
|
+
})
|
|
723
|
+
);
|
|
690
724
|
}
|
|
691
725
|
for (const observation of group.observations) {
|
|
692
|
-
await
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
726
|
+
await tryRun(
|
|
727
|
+
failures,
|
|
728
|
+
"phase-3",
|
|
729
|
+
scope,
|
|
730
|
+
"Observation",
|
|
731
|
+
observation.id ?? "",
|
|
732
|
+
() => createObservationOperation({
|
|
733
|
+
context: workspaceContext,
|
|
734
|
+
body: observation
|
|
735
|
+
})
|
|
736
|
+
);
|
|
696
737
|
}
|
|
697
738
|
for (const encounter of group.encounters) {
|
|
698
|
-
await
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
739
|
+
await tryRun(
|
|
740
|
+
failures,
|
|
741
|
+
"phase-3",
|
|
742
|
+
scope,
|
|
743
|
+
"Encounter",
|
|
744
|
+
encounter.id ?? "",
|
|
745
|
+
() => createEncounterOperation({
|
|
746
|
+
context: workspaceContext,
|
|
747
|
+
body: encounter
|
|
748
|
+
})
|
|
749
|
+
);
|
|
702
750
|
}
|
|
703
751
|
for (const account of group.accounts) {
|
|
704
|
-
await
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
752
|
+
await tryRun(
|
|
753
|
+
failures,
|
|
754
|
+
"phase-3",
|
|
755
|
+
scope,
|
|
756
|
+
"Account",
|
|
757
|
+
account.id ?? "",
|
|
758
|
+
() => createAccountOperation({
|
|
759
|
+
context: workspaceContext,
|
|
760
|
+
body: account
|
|
761
|
+
})
|
|
762
|
+
);
|
|
708
763
|
}
|
|
709
764
|
};
|
|
710
765
|
var seedDemoGraph = async (params) => {
|
|
711
766
|
const { baseContext, devUsers, cognito } = params;
|
|
767
|
+
const failures = [];
|
|
712
768
|
for (const spec of DEMO_TENANT_SPECS) {
|
|
713
769
|
const tenantContext = {
|
|
714
770
|
...baseContext,
|
|
715
771
|
tenantId: spec.tenantId
|
|
716
772
|
};
|
|
717
|
-
await
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
773
|
+
await tryRun(
|
|
774
|
+
failures,
|
|
775
|
+
"phase-1",
|
|
776
|
+
spec.tenantId,
|
|
777
|
+
"Tenant",
|
|
778
|
+
spec.tenantId,
|
|
779
|
+
() => createTenantOperation({
|
|
723
780
|
context: tenantContext,
|
|
724
|
-
body: {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
781
|
+
body: { id: spec.tenantId, resource: tenantResourceBody(spec) }
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
for (const workspace of spec.workspaces) {
|
|
785
|
+
await tryRun(
|
|
786
|
+
failures,
|
|
787
|
+
"phase-1",
|
|
788
|
+
spec.tenantId,
|
|
789
|
+
"Workspace",
|
|
790
|
+
workspace.id,
|
|
791
|
+
() => createWorkspaceOperation({
|
|
792
|
+
context: tenantContext,
|
|
793
|
+
body: {
|
|
794
|
+
id: workspace.id,
|
|
795
|
+
resource: workspaceResourceBody(spec, workspace)
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
);
|
|
729
799
|
}
|
|
730
800
|
}
|
|
731
801
|
for (const user of devUsers) {
|
|
732
|
-
|
|
733
|
-
|
|
802
|
+
let cognitoSub;
|
|
803
|
+
try {
|
|
804
|
+
cognitoSub = await cognito.ensureUser(user.email);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
failures.push({
|
|
807
|
+
phase: "phase-2",
|
|
808
|
+
scope: user.id,
|
|
809
|
+
resourceType: "CognitoUser",
|
|
810
|
+
resourceId: user.email,
|
|
811
|
+
error: err
|
|
812
|
+
});
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
await tryRun(
|
|
816
|
+
failures,
|
|
817
|
+
"phase-2",
|
|
818
|
+
user.id,
|
|
819
|
+
"User",
|
|
820
|
+
user.id,
|
|
821
|
+
() => upsertUser(baseContext, user, cognitoSub)
|
|
822
|
+
);
|
|
734
823
|
for (const spec of DEMO_TENANT_SPECS) {
|
|
735
824
|
const tenantContext = {
|
|
736
825
|
...baseContext,
|
|
737
826
|
tenantId: spec.tenantId
|
|
738
827
|
};
|
|
828
|
+
const userScope = `${user.id}@${spec.tenantId}`;
|
|
739
829
|
const membershipId = demoMembershipId(user.id, spec.tenantId);
|
|
740
|
-
await
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
|
|
748
|
-
const raId = demoRoleAssignmentId(user.id, spec.tenantId, roleCode);
|
|
749
|
-
await createRoleAssignmentOperation({
|
|
830
|
+
await tryRun(
|
|
831
|
+
failures,
|
|
832
|
+
"phase-2",
|
|
833
|
+
userScope,
|
|
834
|
+
"Membership",
|
|
835
|
+
membershipId,
|
|
836
|
+
() => createMembershipOperation({
|
|
750
837
|
context: tenantContext,
|
|
751
838
|
body: {
|
|
752
|
-
id:
|
|
753
|
-
resource:
|
|
754
|
-
spec.scenario,
|
|
755
|
-
spec.tenantId,
|
|
756
|
-
user,
|
|
757
|
-
roleCode,
|
|
758
|
-
raId
|
|
759
|
-
)
|
|
839
|
+
id: membershipId,
|
|
840
|
+
resource: membershipResourceBody(spec, user, membershipId)
|
|
760
841
|
}
|
|
761
|
-
})
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
for (const roleCode of demoRolesForUserInTenant(user, spec.tenantId)) {
|
|
845
|
+
const raId = demoRoleAssignmentId(user.id, spec.tenantId, roleCode);
|
|
846
|
+
await tryRun(
|
|
847
|
+
failures,
|
|
848
|
+
"phase-2",
|
|
849
|
+
userScope,
|
|
850
|
+
"RoleAssignment",
|
|
851
|
+
raId,
|
|
852
|
+
() => createRoleAssignmentOperation({
|
|
853
|
+
context: tenantContext,
|
|
854
|
+
body: {
|
|
855
|
+
id: raId,
|
|
856
|
+
resource: roleAssignmentResourceBody(
|
|
857
|
+
spec.scenario,
|
|
858
|
+
spec.tenantId,
|
|
859
|
+
user,
|
|
860
|
+
roleCode,
|
|
861
|
+
raId
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
);
|
|
762
866
|
}
|
|
763
867
|
}
|
|
764
868
|
const platformContext = {
|
|
@@ -771,22 +875,42 @@ var seedDemoGraph = async (params) => {
|
|
|
771
875
|
PLATFORM_SCOPE_TENANT_ID,
|
|
772
876
|
platformRoleCode
|
|
773
877
|
);
|
|
774
|
-
await
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
platformRaId
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
878
|
+
await tryRun(
|
|
879
|
+
failures,
|
|
880
|
+
"phase-2",
|
|
881
|
+
`${user.id}@${PLATFORM_SCOPE_TENANT_ID}`,
|
|
882
|
+
"RoleAssignment",
|
|
883
|
+
platformRaId,
|
|
884
|
+
() => createRoleAssignmentOperation({
|
|
885
|
+
context: platformContext,
|
|
886
|
+
body: {
|
|
887
|
+
id: platformRaId,
|
|
888
|
+
resource: roleAssignmentResourceBody(
|
|
889
|
+
"platform",
|
|
890
|
+
PLATFORM_SCOPE_TENANT_ID,
|
|
891
|
+
user,
|
|
892
|
+
platformRoleCode,
|
|
893
|
+
platformRaId
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
);
|
|
787
898
|
}
|
|
788
899
|
for (const group of DEMO_DATA_PLANE_FIXTURES) {
|
|
789
|
-
|
|
900
|
+
try {
|
|
901
|
+
await seedWorkspaceDataPlane(baseContext, group, failures);
|
|
902
|
+
} catch (err) {
|
|
903
|
+
failures.push({
|
|
904
|
+
phase: "phase-3",
|
|
905
|
+
scope: `${group.tenantId}/${group.workspaceId}`,
|
|
906
|
+
resourceType: "Workspace",
|
|
907
|
+
resourceId: group.workspaceId,
|
|
908
|
+
error: err
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (failures.length > 0) {
|
|
913
|
+
throw aggregateFailureError(failures);
|
|
790
914
|
}
|
|
791
915
|
};
|
|
792
916
|
var runSeedDemoData = async (event, deps, devUsers) => {
|
|
@@ -914,7 +1038,6 @@ export {
|
|
|
914
1038
|
DEMO_URN_SYSTEM,
|
|
915
1039
|
OPENHI_RESOURCE_URN_SYSTEM,
|
|
916
1040
|
DEMO_PERIOD,
|
|
917
|
-
PLATFORM_SCOPE_TENANT_ID,
|
|
918
1041
|
PLACEHOLDER_TENANT_ID,
|
|
919
1042
|
PLACEHOLDER_WORKSPACE_ID,
|
|
920
1043
|
DEV_USERS,
|
|
@@ -932,4 +1055,4 @@ export {
|
|
|
932
1055
|
productionCognitoProvisioner,
|
|
933
1056
|
handler
|
|
934
1057
|
};
|
|
935
|
-
//# sourceMappingURL=chunk-
|
|
1058
|
+
//# sourceMappingURL=chunk-SXYY5WHG.mjs.map
|