@openhi/constructs 0.0.169 → 0.0.170

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 (51) hide show
  1. package/lib/{chunk-DWSWCUZR.mjs → chunk-6HGSR3TG.mjs} +2 -2
  2. package/lib/chunk-APVVG7BO.mjs +61 -0
  3. package/lib/chunk-APVVG7BO.mjs.map +1 -0
  4. package/lib/{chunk-ZODGX37H.mjs → chunk-E2OWEBBH.mjs} +3 -3
  5. package/lib/{chunk-GJTPXJKD.mjs → chunk-EBB4RNUG.mjs} +2 -2
  6. package/lib/{chunk-O5VQWB6U.mjs → chunk-FDBBTNCI.mjs} +5 -61
  7. package/lib/chunk-FDBBTNCI.mjs.map +1 -0
  8. package/lib/{chunk-P3CTZWC2.mjs → chunk-GG2WD6TA.mjs} +2 -2
  9. package/lib/{chunk-Q64MOYJ7.mjs → chunk-JUSVETWK.mjs} +3 -3
  10. package/lib/{chunk-KA3OMP3X.mjs → chunk-USNOOCSZ.mjs} +9 -3
  11. package/lib/chunk-USNOOCSZ.mjs.map +1 -0
  12. package/lib/{chunk-2O3CXY2C.mjs → chunk-XJ5SRUGN.mjs} +2 -2
  13. package/lib/{chunk-2O3CXY2C.mjs.map → chunk-XJ5SRUGN.mjs.map} +1 -1
  14. package/lib/{chunk-P3NFCKTZ.mjs → chunk-XNUCKVSE.mjs} +2 -2
  15. package/lib/{chunk-WFTDH2NM.mjs → chunk-Y4RGUAM2.mjs} +2 -2
  16. package/lib/{chunk-XHG4SODS.mjs → chunk-Z4PZSLYY.mjs} +2 -2
  17. package/lib/counter-reconciliation.handler.mjs +5 -4
  18. package/lib/counter-reconciliation.handler.mjs.map +1 -1
  19. package/lib/data-store-postgres-replication.handler.js +42 -4
  20. package/lib/data-store-postgres-replication.handler.js.map +1 -1
  21. package/lib/data-store-postgres-replication.handler.mjs +9 -4
  22. package/lib/data-store-postgres-replication.handler.mjs.map +1 -1
  23. package/lib/firehose-archive-transform.handler.d.mts +2 -1
  24. package/lib/firehose-archive-transform.handler.d.ts +2 -1
  25. package/lib/firehose-archive-transform.handler.js +42 -2
  26. package/lib/firehose-archive-transform.handler.js.map +1 -1
  27. package/lib/firehose-archive-transform.handler.mjs +4 -1
  28. package/lib/index.js +21 -2
  29. package/lib/index.js.map +1 -1
  30. package/lib/index.mjs +26 -6
  31. package/lib/index.mjs.map +1 -1
  32. package/lib/pre-token-generation.handler.mjs +6 -5
  33. package/lib/pre-token-generation.handler.mjs.map +1 -1
  34. package/lib/provision-default-workspace.handler.mjs +5 -4
  35. package/lib/provision-default-workspace.handler.mjs.map +1 -1
  36. package/lib/rest-api-lambda.handler.js +1 -1
  37. package/lib/rest-api-lambda.handler.js.map +1 -1
  38. package/lib/rest-api-lambda.handler.mjs +14 -12
  39. package/lib/rest-api-lambda.handler.mjs.map +1 -1
  40. package/lib/seed-demo-data.handler.mjs +5 -4
  41. package/package.json +1 -1
  42. package/lib/chunk-KA3OMP3X.mjs.map +0 -1
  43. package/lib/chunk-O5VQWB6U.mjs.map +0 -1
  44. /package/lib/{chunk-DWSWCUZR.mjs.map → chunk-6HGSR3TG.mjs.map} +0 -0
  45. /package/lib/{chunk-ZODGX37H.mjs.map → chunk-E2OWEBBH.mjs.map} +0 -0
  46. /package/lib/{chunk-GJTPXJKD.mjs.map → chunk-EBB4RNUG.mjs.map} +0 -0
  47. /package/lib/{chunk-P3CTZWC2.mjs.map → chunk-GG2WD6TA.mjs.map} +0 -0
  48. /package/lib/{chunk-Q64MOYJ7.mjs.map → chunk-JUSVETWK.mjs.map} +0 -0
  49. /package/lib/{chunk-P3NFCKTZ.mjs.map → chunk-XNUCKVSE.mjs.map} +0 -0
  50. /package/lib/{chunk-WFTDH2NM.mjs.map → chunk-Y4RGUAM2.mjs.map} +0 -0
  51. /package/lib/{chunk-XHG4SODS.mjs.map → chunk-Z4PZSLYY.mjs.map} +0 -0
@@ -8,15 +8,16 @@ import {
8
8
  productionCognitoProvisioner,
9
9
  runSeedDemoData,
10
10
  seedDemoGraph
11
- } from "./chunk-ZODGX37H.mjs";
12
- import "./chunk-P3NFCKTZ.mjs";
13
- import "./chunk-XHG4SODS.mjs";
11
+ } from "./chunk-E2OWEBBH.mjs";
12
+ import "./chunk-XNUCKVSE.mjs";
13
+ import "./chunk-Z4PZSLYY.mjs";
14
14
  import "./chunk-BUAYVN3C.mjs";
15
15
  import "./chunk-5S6VFBLT.mjs";
16
16
  import "./chunk-I6LUPJUY.mjs";
17
17
  import "./chunk-QJDHVMKT.mjs";
18
18
  import "./chunk-6BB4CRSS.mjs";
19
- import "./chunk-O5VQWB6U.mjs";
19
+ import "./chunk-FDBBTNCI.mjs";
20
+ import "./chunk-APVVG7BO.mjs";
20
21
  import "./chunk-FYHBHHWK.mjs";
21
22
  import "./chunk-EUIP2U5F.mjs";
22
23
  import "./chunk-TRY7JGWO.mjs";
package/package.json CHANGED
@@ -71,7 +71,7 @@
71
71
  "publishConfig": {
72
72
  "access": "public"
73
73
  },
74
- "version": "0.0.169",
74
+ "version": "0.0.170",
75
75
  "types": "lib/index.d.ts",
76
76
  "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
77
77
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/dynamodb/firehose-archive-transform.handler.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { AttributeValue } from \"@aws-sdk/client-dynamodb\";\nimport {\n EventBridgeClient,\n PutEventsCommand,\n type PutEventsRequestEntry,\n type PutEventsResultEntry,\n} from \"@aws-sdk/client-eventbridge\";\nimport { PutObjectCommand, S3Client } from \"@aws-sdk/client-s3\";\n// Canonical bus-source constants live in `@openhi/workflows` per\n// TR-016 §Configuration Standards; this handler imports the data-bus\n// `Source` value from there rather than a local re-export so every\n// publisher and every consumer reads from the same symbol.\nimport { OPENHI_DATA_SOURCE } from \"@openhi/workflows\";\nimport type {\n FirehoseTransformationEvent,\n FirehoseTransformationResult,\n FirehoseTransformationResultRecord,\n} from \"aws-lambda\";\nimport {\n DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES,\n DATA_STORE_CHANGE_DETAIL_TYPE,\n buildFhirCurrentResourceChangeDetail,\n} from \"./data-store-change-events\";\nimport {\n type DynamoDbStreamKinesisRecord,\n dynamodbImageToPlain,\n} from \"./dynamodb-stream-record\";\n\nexport type { DynamoDbStreamKinesisRecord } from \"./dynamodb-stream-record\";\n\n/**\n * Firehose data-transformation handler: filters DynamoDB change records to\n * current FHIR resource items (SK = CURRENT, TID#…#WID#…#RT#…#ID#… PK),\n * writes archive JSON to S3 via Firehose, sets dynamic partition keys per\n * ADR 2026-03-11-02, and publishes de-identified change notifications to the\n * data event bus via PutEvents per ADR 2026-03-02-01, with retries and an S3\n * dead-letter bucket for entries that still fail.\n */\n\n/**\n * PK pattern emitted by the data-entity factory and ElectroDB. The literal\n * segments (`TID#`, `WID#`, `RT#`, `ID#`) are matched case-insensitively\n * because ElectroDB lowercases the entire composite key by default (the\n * factory's GSI1SK uses `casing: \"none\"` to opt out, but the base-table PK\n * does not — see `data-entity-common.ts` and the test at line 56 of its spec\n * for the lowercased `tid#…#wid#…#rt#…#shard#…` shape).\n */\nconst PK_PATTERN =\n /^TID#(?<tenantId>[^#]+)#WID#(?<workspaceId>[^#]+)#RT#(?<resourceType>[^#]+)#ID#(?<resourceId>.+)$/i;\n\n/**\n * SK shape that identifies a \"current\" (non-versioned) item. The data-entity\n * factory uses a single-attribute composite SK (`sk` → default `\"CURRENT\"`)\n * with no explicit template, so ElectroDB wraps the value into its standard\n * `$<entity>_<version>#sk_<lowercased-value>` format — e.g.\n * `$patient_01#sk_current`. The historical bare-`CURRENT` form is still\n * accepted so any consumer writing directly to DynamoDB without ElectroDB\n * (tests, integration fixtures, ad-hoc admin writes) does not get silently\n * filtered out.\n */\nconst CURRENT_SK_PATTERN = /^(CURRENT|\\$[A-Za-z0-9_-]+_[0-9]+#sk_current)$/i;\n\n/** DynamoDB-managed attribute on global table items (see AWS Global Tables legacy / replication docs). */\nconst AWS_REP_UPDATE_REGION = \"aws:rep:updateregion\";\n\nfunction getDynamoDbStringAttr(\n image: Record<string, AttributeValue> | undefined,\n name: string,\n): string | undefined {\n if (!image) {\n return undefined;\n }\n const av = image[name];\n if (typeof av?.S === \"string\" && av.S.trim() !== \"\") {\n return av.S.trim();\n }\n return undefined;\n}\n\nfunction primaryImageForReplicationCheck(\n record: DynamoDbStreamKinesisRecord,\n): Record<string, AttributeValue> | undefined {\n if (record.eventName === \"REMOVE\") {\n return record.dynamodb?.OldImage;\n }\n return record.dynamodb?.NewImage;\n}\n\n/**\n * Returns true when this stream/Kinesis record should not be archived because it\n * represents a **replica-side application** of a global-table change (the logical\n * write originated in another Region).\n *\n * - If `aws:rep:updateregion` is present on the item image and differs from\n * `archiveLambdaRegion`, the change was replicated into this Region (archive\n * only in the Region that matches `aws:rep:updateregion`).\n * - Otherwise, if `userIdentity` matches the DynamoDB replication service SLR,\n * treat as replication. **Excluded:** TTL deletes (`type` Service and\n * `principalId` `dynamodb.amazonaws.com`) per AWS stream Identity docs.\n *\n * For MREC global tables version 2019.11.21, AWS documents that stream records\n * may not carry distinguishable metadata; the recommended approach is a custom\n * “write region” attribute on items. If neither that attribute nor\n * `aws:rep:updateregion` nor replication `userIdentity` applies, this function\n * returns false (no drop)—duplicate archives are possible if identical pipelines\n * run in every Region without those signals.\n */\nexport function shouldDropAsGlobalTableReplicationRecord(\n record: DynamoDbStreamKinesisRecord,\n archiveLambdaRegion: string,\n): boolean {\n const image = primaryImageForReplicationCheck(record);\n const updateRegion = getDynamoDbStringAttr(image, AWS_REP_UPDATE_REGION);\n if (\n updateRegion &&\n archiveLambdaRegion &&\n updateRegion !== archiveLambdaRegion\n ) {\n return true;\n }\n\n return isDynamoDbReplicationUserIdentity(record.userIdentity);\n}\n\nfunction isDynamoDbReplicationUserIdentity(userIdentity: unknown): boolean {\n if (!userIdentity || typeof userIdentity !== \"object\") {\n return false;\n }\n const ui = userIdentity as Record<string, unknown>;\n const principalRaw = ui.principalId ?? ui.PrincipalId;\n const typeRaw = ui.type ?? ui.Type;\n const principal =\n typeof principalRaw === \"string\" ? principalRaw.toLowerCase() : \"\";\n const type = typeof typeRaw === \"string\" ? typeRaw.toLowerCase() : \"\";\n\n if (type === \"service\" && principal === \"dynamodb.amazonaws.com\") {\n return false;\n }\n\n const replicationMarkers = [\n \"awsservicerolefordynamodbreplication\",\n \"replication.dynamodb.amazonaws.com\",\n ];\n return replicationMarkers.some((m) => principal.includes(m));\n}\n\nexport function parseCurrentResourceKeys(record: DynamoDbStreamKinesisRecord): {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n} | null {\n const keys = record.dynamodb?.Keys;\n if (!keys) {\n return null;\n }\n const pkAttr = keys.PK?.S;\n const skAttr = keys.SK?.S;\n if (!pkAttr || !skAttr || !CURRENT_SK_PATTERN.test(skAttr)) {\n return null;\n }\n const m = PK_PATTERN.exec(pkAttr);\n if (!m?.groups) {\n return null;\n }\n const {\n tenantId,\n workspaceId,\n resourceType: pkResourceType,\n resourceId,\n } = m.groups;\n const image =\n record.eventName === \"REMOVE\"\n ? record.dynamodb?.OldImage\n : record.dynamodb?.NewImage;\n if (!image) {\n return null;\n }\n const plain = dynamodbImageToPlain(image as Record<string, AttributeValue>);\n const version = typeof plain.vid === \"string\" ? plain.vid : null;\n if (!version) {\n return null;\n }\n // The PK segment carries a lowercased copy of the resource type (ElectroDB\n // default casing), which loses the FHIR-spec PascalCase shape consumers\n // depend on (`resource_type` column matches in Postgres search, EventBridge\n // rules pattern-matching, archive partition naming). Recover the canonical\n // case from the resource JSON itself when available; fall back to the PK\n // token if the resource is missing or unparseable so soft-deletes and other\n // edge cases still produce a key tuple.\n const resourceType =\n extractFhirResourceTypeFromImage(plain) ?? pkResourceType;\n return { tenantId, workspaceId, resourceType, resourceId, version };\n}\n\nfunction extractFhirResourceTypeFromImage(\n plain: Record<string, unknown>,\n): string | undefined {\n const resourceStr =\n typeof plain.resource === \"string\" ? plain.resource : undefined;\n if (!resourceStr) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(resourceStr) as { resourceType?: unknown };\n if (typeof parsed.resourceType === \"string\" && parsed.resourceType !== \"\") {\n return parsed.resourceType;\n }\n } catch {\n // Malformed resource JSON — defer to caller's fallback.\n }\n return undefined;\n}\n\nfunction partitionToken(value: string): string {\n if (!value || value.trim() === \"\") {\n return \"-\";\n }\n return value.replace(/[/\\\\]/g, \"_\");\n}\n\nfunction buildArchivePayload(\n record: DynamoDbStreamKinesisRecord,\n keys: ReturnType<typeof parseCurrentResourceKeys>,\n): Record<string, unknown> {\n const newImage = record.dynamodb?.NewImage;\n const oldImage = record.dynamodb?.OldImage;\n const resourceImage = record.eventName === \"REMOVE\" ? oldImage : newImage;\n const resourcePlain = resourceImage\n ? dynamodbImageToPlain(resourceImage as Record<string, AttributeValue>)\n : {};\n\n if (typeof resourcePlain.resource === \"string\") {\n try {\n resourcePlain.resource = JSON.parse(resourcePlain.resource) as unknown;\n } catch {\n /* keep raw string if not valid JSON */\n }\n }\n\n return {\n eventName: record.eventName,\n archivedAt: new Date().toISOString(),\n tenantId: keys!.tenantId,\n workspaceId: keys!.workspaceId,\n resourceType: keys!.resourceType,\n resourceId: keys!.resourceId,\n version: keys!.version,\n resource: resourcePlain,\n };\n}\n\nconst PUT_EVENTS_BATCH_SIZE = 10;\n\n/** Full PutEvents rounds per chunk (initial attempt + failure-driven retries). */\nconst MAX_PUT_EVENTS_ROUNDS = 3;\n\nlet eventBridgeClient: EventBridgeClient | undefined;\n\nfunction getEventBridgeClient(): EventBridgeClient | undefined {\n const bus = process.env.DATA_EVENT_BUS_NAME?.trim();\n if (!bus) {\n return undefined;\n }\n if (!eventBridgeClient) {\n eventBridgeClient = new EventBridgeClient({});\n }\n return eventBridgeClient;\n}\n\nlet s3ClientForDlq: S3Client | undefined;\n\nfunction getS3ClientForDlq(): S3Client | undefined {\n const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();\n if (!bucket) {\n return undefined;\n }\n if (!s3ClientForDlq) {\n s3ClientForDlq = new S3Client({});\n }\n return s3ClientForDlq;\n}\n\ntype PutEventsEntry = PutEventsRequestEntry;\n\ninterface PutEventsDlqPayload {\n dlqSchemaVersion: 1;\n failedAt: string;\n reason: \"put_events_partial_failure\" | \"put_events_sdk_error\";\n attemptRounds: number;\n entries: PutEventsEntry[];\n putEventsResultEntries?: PutEventsResultEntry[];\n sdkError?: string;\n}\n\nasync function writePutEventsFailuresToDlq(\n payload: PutEventsDlqPayload,\n): Promise<void> {\n const bucket = process.env.DATA_STORE_PUT_EVENTS_DLQ_BUCKET?.trim();\n const client = getS3ClientForDlq();\n if (!bucket || !client) {\n throw new Error(\n `PutEvents exhausted retries but DATA_STORE_PUT_EVENTS_DLQ_BUCKET is not set (${payload.reason})`,\n );\n }\n const day = payload.failedAt.slice(0, 10);\n const key = `put-events-failed/${day}/${randomUUID()}.json`;\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(payload),\n ContentType: \"application/json\",\n }),\n );\n}\n\n/**\n * Sends one PutEvents batch (≤10 entries) with up to {@link MAX_PUT_EVENTS_ROUNDS}\n * rounds. After the last round, remaining failures or a final SDK error are\n * written to the DLQ S3 bucket (if configured); DLQ write failure throws.\n */\nasync function putEventsChunkWithRetriesAndDlq(\n client: EventBridgeClient,\n entries: PutEventsEntry[],\n): Promise<void> {\n if (entries.length === 0) {\n return;\n }\n\n let pending = [...entries];\n\n for (let round = 1; round <= MAX_PUT_EVENTS_ROUNDS; round++) {\n try {\n const out = await client.send(new PutEventsCommand({ Entries: pending }));\n const failed = out.FailedEntryCount ?? 0;\n if (failed === 0) {\n return;\n }\n\n const nextPending: PutEventsEntry[] = [];\n out.Entries?.forEach((e: PutEventsResultEntry | undefined, i: number) => {\n if (e?.ErrorCode && pending[i]) {\n nextPending.push(pending[i]!);\n }\n });\n pending = nextPending;\n\n if (pending.length === 0) {\n return;\n }\n\n if (round === MAX_PUT_EVENTS_ROUNDS) {\n await writePutEventsFailuresToDlq({\n dlqSchemaVersion: 1,\n failedAt: new Date().toISOString(),\n reason: \"put_events_partial_failure\",\n attemptRounds: MAX_PUT_EVENTS_ROUNDS,\n entries: pending,\n putEventsResultEntries: out.Entries,\n });\n return;\n }\n } catch (sdkErr) {\n const sdkMessage =\n sdkErr instanceof Error ? sdkErr.message : String(sdkErr);\n if (round === MAX_PUT_EVENTS_ROUNDS) {\n await writePutEventsFailuresToDlq({\n dlqSchemaVersion: 1,\n failedAt: new Date().toISOString(),\n reason: \"put_events_sdk_error\",\n attemptRounds: MAX_PUT_EVENTS_ROUNDS,\n entries: pending,\n sdkError: sdkMessage,\n });\n return;\n }\n await new Promise((r) => setTimeout(r, 50 * round));\n }\n }\n}\n\nasync function publishDataStoreChangeEvents(\n pending: Array<{\n change: DynamoDbStreamKinesisRecord;\n keys: NonNullable<ReturnType<typeof parseCurrentResourceKeys>>;\n }>,\n): Promise<void> {\n const client = getEventBridgeClient();\n const busName = process.env.DATA_EVENT_BUS_NAME?.trim();\n if (!client || !busName || pending.length === 0) {\n return;\n }\n\n const entries: PutEventsEntry[] = [];\n for (const { change, keys } of pending) {\n const detailObj = buildFhirCurrentResourceChangeDetail(change, keys);\n const detail = JSON.stringify(detailObj);\n const detailBytes = Buffer.byteLength(detail, \"utf8\");\n if (detailBytes > DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES) {\n throw new Error(\n `Event detail is ${detailBytes} bytes (max ${DATA_STORE_CHANGE_DETAIL_MAX_UTF8_BYTES}); ` +\n `oversize strategy deferred per ADR 2026-03-02-01 (${keys.resourceType}/${keys.resourceId}).`,\n );\n }\n entries.push({\n Source: OPENHI_DATA_SOURCE,\n DetailType: DATA_STORE_CHANGE_DETAIL_TYPE,\n Detail: detail,\n EventBusName: busName,\n });\n }\n\n for (let i = 0; i < entries.length; i += PUT_EVENTS_BATCH_SIZE) {\n const chunk = entries.slice(i, i + PUT_EVENTS_BATCH_SIZE);\n await putEventsChunkWithRetriesAndDlq(client, chunk);\n }\n}\n\nexport async function handler(\n event: FirehoseTransformationEvent,\n): Promise<FirehoseTransformationResult> {\n const records: FirehoseTransformationResultRecord[] = [];\n const archiveLambdaRegion = process.env.AWS_REGION ?? \"\";\n const pendingPublish: Array<{\n change: DynamoDbStreamKinesisRecord;\n keys: NonNullable<ReturnType<typeof parseCurrentResourceKeys>>;\n }> = [];\n\n for (const rec of event.records) {\n try {\n const payload = Buffer.from(rec.data, \"base64\").toString(\"utf8\");\n const change = JSON.parse(payload) as DynamoDbStreamKinesisRecord;\n\n if (\n shouldDropAsGlobalTableReplicationRecord(change, archiveLambdaRegion)\n ) {\n records.push({\n recordId: rec.recordId,\n result: \"Dropped\",\n data: rec.data,\n });\n continue;\n }\n\n const keys = parseCurrentResourceKeys(change);\n\n if (!keys) {\n records.push({\n recordId: rec.recordId,\n result: \"Dropped\",\n data: rec.data,\n });\n continue;\n }\n\n const archive = buildArchivePayload(change, keys);\n const out = Buffer.from(`${JSON.stringify(archive)}\\n`).toString(\n \"base64\",\n );\n\n pendingPublish.push({ change, keys });\n\n records.push({\n recordId: rec.recordId,\n result: \"Ok\",\n data: out,\n metadata: {\n partitionKeys: {\n tenantId: partitionToken(keys.tenantId),\n workspaceId: partitionToken(keys.workspaceId),\n resourceType: partitionToken(keys.resourceType),\n resourceId: partitionToken(keys.resourceId),\n version: partitionToken(keys.version),\n },\n },\n });\n } catch {\n records.push({\n recordId: rec.recordId,\n result: \"ProcessingFailed\",\n data: rec.data,\n });\n }\n }\n\n await publishDataStoreChangeEvents(pendingPublish);\n\n return { records };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/data/audit-meta.ts","../src/data/operations/data-operations-common.ts","../src/lib/compression.ts"],"sourcesContent":["import type { Extension, Meta } from \"@openhi/types\";\n\n/**\n * Shared audit/meta helpers for FHIR resources. Used by data operations and import.\n * OpenHI audit is stored in resource meta.extension (per ADR 2026-01-13-06).\n *\n * @see sites/www-docs/content/packages/@openhi/constructs/data/dynamo/entity-standards.md\n */\n\n/** OpenHI extension URLs for audit in resource meta (per ADR 2026-01-13-06). */\nconst OPENHI_EXT = \"http://openhi.org/fhir/StructureDefinition\";\n\n/** Meta with optional OpenHI audit extensions (created/modified by, etc.). */\nexport type MetaWithExtensions = Meta & { extension?: Array<Extension> };\n\n/** Audit fields stored in FHIR resource meta.extension. */\nexport interface AuditFields {\n createdDate?: string;\n createdById?: string;\n createdByName?: string;\n modifiedDate?: string;\n modifiedById?: string;\n modifiedByName?: string;\n deletedDate?: string;\n deletedById?: string;\n deletedByName?: string;\n}\n\n/** Audit extension entry shape (subset of Extension used by OpenHI audit). */\ntype AuditExtensionEntry = Pick<\n Extension,\n \"url\" | \"valueString\" | \"valueDateTime\"\n>;\n\n/** Builds meta.extension entries for audit; merges with existing meta. */\nexport function mergeAuditIntoMeta(\n meta: MetaWithExtensions | Record<string, unknown> | undefined,\n audit: AuditFields,\n): MetaWithExtensions {\n const existing = (meta ?? {}) as MetaWithExtensions;\n const ext: Array<AuditExtensionEntry> = [\n ...(Array.isArray(existing.extension)\n ? (existing.extension as Array<AuditExtensionEntry>)\n : []),\n ];\n const byUrl = new Map(ext.map((e) => [e.url, e]));\n function set(\n url: string,\n value: string | undefined,\n type: \"valueString\" | \"valueDateTime\",\n ) {\n if (value == null) return;\n byUrl.set(url, { url, [type]: value });\n }\n set(`${OPENHI_EXT}/created-date`, audit.createdDate, \"valueDateTime\");\n set(`${OPENHI_EXT}/created-by-id`, audit.createdById, \"valueString\");\n set(`${OPENHI_EXT}/created-by-name`, audit.createdByName, \"valueString\");\n set(`${OPENHI_EXT}/modified-date`, audit.modifiedDate, \"valueDateTime\");\n set(`${OPENHI_EXT}/modified-by-id`, audit.modifiedById, \"valueString\");\n set(`${OPENHI_EXT}/modified-by-name`, audit.modifiedByName, \"valueString\");\n set(`${OPENHI_EXT}/deleted-date`, audit.deletedDate, \"valueDateTime\");\n set(`${OPENHI_EXT}/deleted-by-id`, audit.deletedById, \"valueString\");\n set(`${OPENHI_EXT}/deleted-by-name`, audit.deletedByName, \"valueString\");\n return { ...existing, extension: Array.from(byUrl.values()) };\n}\n","import { extractSortKey, extractSummary } from \"@openhi/types\";\nimport type { Meta, FhirResourceLike } from \"@openhi/types\";\nimport { compressResource, decompressResource } from \"../../lib/compression\";\nimport { mergeAuditIntoMeta, type MetaWithExtensions } from \"../audit-meta\";\nimport { SHARD_COUNT } from \"../dynamo/shard\";\nimport { NotFoundError } from \"../errors\";\nimport type { OpenHiContext } from \"../openhi-context\";\n\n/**\n * Sort key for the current record version. Matches Dynamo record index SK default.\n * Use this in get/update/delete (and create where applicable) for data-plane entities.\n */\nexport const DATA_ENTITY_SK = \"CURRENT\" as const;\n\n/** Base params for data-entity operations: context and optional table override. */\nexport interface BaseDataEntityParams {\n context: OpenHiContext;\n /** Optional table name override; resolved by data service from DYNAMO_TABLE_NAME when omitted. */\n tableName?: string;\n}\n\n/** Params for get-by-id and delete (context + id + optional tableName). */\nexport interface GetByIdParams extends BaseDataEntityParams {\n id: string;\n}\n\n/**\n * Mode for list operations introduced by #853 to back FHIR `_summary` opt-ins.\n *\n * - `full` (default): GSI1 fan-out → BatchGet hydration → full FHIR resource per entry.\n * - `summary`: GSI1 fan-out only; the `summary` JSON projected onto GSI1 is parsed and used\n * as `resource` per entry. Skips BatchGet entirely — that's the cost win the FHIR spec\n * intends `_summary=true` to deliver.\n * - `count`: GSI1 fan-out only; entries are dropped, only `total` is populated. Routes\n * should pass `total` into `buildSearchsetBundle({ mode: \"count\", total })`.\n *\n * `_elements` is implemented at the route layer as `full` mode + post-hydration pruning,\n * since per-element pruning has to happen after decompression and is FHIR-spec-defined\n * (always retain root-level mandatories — see `prune-resource-by-elements.ts`).\n */\nexport type ListOperationMode = \"full\" | \"summary\" | \"count\";\n\n/** Params for list (context + optional tableName + optional mode for #853 `_summary`). */\nexport interface ListParams extends BaseDataEntityParams {\n /** Defaults to `\"full\"` (current behavior); routes pass other modes for `_summary` opt-ins. */\n mode?: ListOperationMode;\n}\n\n/** Result for create / get-by-id / update: single resource. */\nexport interface SingleResourceResult<T> {\n id: string;\n resource: T;\n}\n\n/** Entry shape for list results. */\nexport interface ListEntry<T> {\n id: string;\n resource: T;\n}\n\n/**\n * Result for list: entries array plus total count.\n *\n * - For `mode === \"full\"` and `mode === \"summary\"`, `total === entries.length`.\n * - For `mode === \"count\"`, `entries` is empty and `total` is the GSI1 fan-out count.\n *\n * Splitting `total` from `entries.length` lets count-mode routes report a true count\n * while skipping any per-entry work.\n */\nexport interface ListResult<T> {\n entries: Array<ListEntry<T>>;\n total: number;\n}\n\n/** Minimal entity shape for get (used by getDataEntityById). */\ninterface EntityWithGet {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: { id: string; resource: string } | null }> };\n}\n\n/** Minimal entity shape for delete (used by deleteDataEntityById). */\ninterface EntityWithDelete {\n delete(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<unknown> };\n}\n\n/**\n * Minimal entity shape for list via GSI1 + BatchGet hydration (used by listDataEntitiesByWorkspace).\n * GSI1 is sharded per ADR-011, so listing fans out to each shard and concatenates ids; the\n * `resource` attribute is NOT projected onto GSI1 (per the summary projection in\n * `dynamo-db-data-store.ts`), so the second phase BatchGets the base table for full items.\n *\n * GSI1 INCLUDE projection (per `dynamo-db-data-store.ts`) carries `summary`, `vid`, `lastUpdated`,\n * `createdDate`, `modifiedDate`, `createdById`, `modifiedById` alongside the key attributes.\n * `summary` is what `mode: \"summary\"` returns to the caller without hitting the base table.\n */\ninterface DataEntityWithListAndBatchGet {\n query: {\n gsi1(params: {\n tenantId: string;\n workspaceId: string;\n gsi1Shard: string;\n }): {\n go(): Promise<{\n data: Array<{\n id: string;\n summary?: string;\n vid?: string;\n lastUpdated?: string;\n createdDate?: string;\n modifiedDate?: string;\n createdById?: string;\n modifiedById?: string;\n }> | null;\n }>;\n };\n };\n get(\n keys: Array<{\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }>,\n ): {\n go(): Promise<{\n data: Array<{ id: string; resource: string }>;\n unprocessed: Array<{\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }>;\n }>;\n };\n}\n\n/** Minimal entity shape for put (used by createDataEntityRecord). */\ninterface EntityWithPut {\n put(attrs: {\n sk: string;\n tenantId: string;\n workspaceId: string;\n id: string;\n resource: string;\n summary: string;\n vid: string;\n lastUpdated: string;\n gsi1sk: string;\n }): { go(): Promise<unknown> };\n}\n\n/** Minimal entity shape for patch (used by updateDataEntityById). */\ninterface EntityWithPatch {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: { id: string; resource: string } | null }> };\n patch(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): {\n set(attrs: {\n resource: string;\n summary: string;\n lastUpdated: string;\n gsi1sk: string;\n }): {\n go(): Promise<unknown>;\n };\n };\n}\n\n/**\n * Get a single data-entity record by id. Decompresses and parses resource; throws NotFoundError if missing.\n * Use from get-by-id operations with the appropriate entity and resource type.\n */\nexport async function getDataEntityById<T>(\n entity: EntityWithGet,\n tenantId: string,\n workspaceId: string,\n id: string,\n resourceLabel: string,\n): Promise<SingleResourceResult<T>> {\n const result = await entity\n .get({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (!result.data) {\n throw new NotFoundError(`${resourceLabel} ${id} not found`, {\n details: { id },\n });\n }\n\n const parsed = JSON.parse(decompressResource(result.data.resource)) as T & {\n id?: string;\n };\n return {\n id: result.data.id,\n resource: { ...parsed, id: result.data.id } as T,\n };\n}\n\n/**\n * Delete a data-entity record by id. Idempotent (no-op if not found).\n * Use from delete operations with the appropriate entity.\n */\nexport async function deleteDataEntityById(\n entity: EntityWithDelete,\n tenantId: string,\n workspaceId: string,\n id: string,\n): Promise<void> {\n await entity\n .delete({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .go();\n}\n\n/** Maximum total attempts (initial + retries) when hydrating list ids via BatchGet. */\nconst BATCH_GET_MAX_ATTEMPTS = 3;\n/** Base backoff in milliseconds applied to BatchGet retries; doubles each attempt. */\nconst BATCH_GET_BASE_BACKOFF_MS = 50;\n\n/** Minimal entity shape for BatchGet hydration on the base table; chunks of 100 are handled by ElectroDB. */\ninterface EntityWithBatchGet<TKey, TItem> {\n get(keys: TKey[]): {\n go(): Promise<{ data: TItem[]; unprocessed: TKey[] }>;\n };\n}\n\n/**\n * BatchGet wrapper that retries `UnprocessedKeys` with exponential backoff. ElectroDB chunks the\n * input keys into groups of 100 internally, but does not retry unprocessed keys — DynamoDB can\n * return some keys unprocessed under throttling or partial failure, and the caller must reissue\n * them. Throws after `BATCH_GET_MAX_ATTEMPTS` if any keys remain unprocessed; intended for list\n * hydration (#854) where partial results would silently truncate the response.\n */\nexport async function batchGetWithRetry<TKey, TItem>(\n entity: EntityWithBatchGet<TKey, TItem>,\n keys: TKey[],\n): Promise<TItem[]> {\n if (keys.length === 0) return [];\n\n const collected: TItem[] = [];\n let pending = keys;\n let attempt = 0;\n\n while (pending.length > 0) {\n if (attempt > 0) {\n await new Promise((resolve) =>\n setTimeout(resolve, BATCH_GET_BASE_BACKOFF_MS * 2 ** (attempt - 1)),\n );\n }\n attempt++;\n const result = await entity.get(pending).go();\n collected.push(...result.data);\n const unprocessed = result.unprocessed ?? [];\n if (unprocessed.length === 0) break;\n if (attempt >= BATCH_GET_MAX_ATTEMPTS) {\n throw new Error(\n `BatchGet exhausted retries: ${unprocessed.length} key(s) still unprocessed after ${BATCH_GET_MAX_ATTEMPTS} attempt(s)`,\n );\n }\n pending = unprocessed;\n }\n\n return collected;\n}\n\n/** GSI1 index item shape — what a sharded `gsi1.query().go()` returns per row. */\nexport interface ShardedListIndexItem {\n id: string;\n summary?: string;\n}\n\n/** Hooks that adapt a generic mode-dispatching list to a specific entity's hydration and entry shape. */\nexport interface DispatchListModeHooks<TItem, TEntry> {\n /** Hydrate the base table for the given ids (typically `batchGetWithRetry(entity, keys)`). */\n hydrate: (orderedIds: string[]) => Promise<TItem[]>;\n /** Extract the canonical id from a hydrated item so it can be matched back to the GSI1 order. */\n getId: (item: TItem) => string;\n /** Build the result entry for `mode === \"full\"` from a hydrated base-table item. */\n buildEntry: (id: string, item: TItem) => TEntry;\n /** Build the result entry for `mode === \"summary\"` from the parsed GSI1 `summary` JSON. */\n buildSummaryEntry: (\n id: string,\n parsedSummary: Record<string, unknown>,\n ) => TEntry;\n}\n\n/**\n * Mode dispatcher shared by data-plane and control-plane list operations (#853).\n *\n * Given pre-fetched `shardResults` from a sharded GSI1 fan-out, returns either:\n * - `mode === \"count\"` — `{ entries: [], total }` where total is the sum of shard row counts.\n * - `mode === \"summary\"` — entries built from each shard row's parsed `summary` JSON; rows with\n * missing or unparseable `summary` are dropped.\n * - `mode === \"full\"` — calls `hydrate(orderedIds)` (typically a BatchGet) and builds entries\n * from hydrated items in per-shard GSI1 sort order; missing items are dropped.\n *\n * Lives here (alongside `listDataEntitiesByWorkspace`) because the same dispatch logic is needed\n * by every list operation that backs a FHIR list/search endpoint, including the seven\n * control-plane peers (User, Role, Tenant, Workspace, Membership, RoleAssignment, Configuration).\n */\nexport async function dispatchListMode<TItem, TEntry>(\n mode: ListOperationMode,\n shardResults: Array<{ data: Array<ShardedListIndexItem> | null }>,\n hooks: DispatchListModeHooks<TItem, TEntry>,\n): Promise<{ entries: TEntry[]; total: number }> {\n if (mode === \"count\") {\n let total = 0;\n for (const shardResult of shardResults) {\n total += (shardResult.data ?? []).length;\n }\n return { entries: [], total };\n }\n\n if (mode === \"summary\") {\n const entries: TEntry[] = [];\n for (const shardResult of shardResults) {\n for (const item of shardResult.data ?? []) {\n if (typeof item.summary !== \"string\") continue;\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(item.summary) as Record<string, unknown>;\n } catch {\n continue;\n }\n entries.push(hooks.buildSummaryEntry(item.id, parsed));\n }\n }\n return { entries, total: entries.length };\n }\n\n const orderedIds: string[] = [];\n for (const shardResult of shardResults) {\n for (const item of shardResult.data ?? []) {\n orderedIds.push(item.id);\n }\n }\n\n if (orderedIds.length === 0) return { entries: [], total: 0 };\n\n const items = await hooks.hydrate(orderedIds);\n const byId = new Map(items.map((item) => [hooks.getId(item), item]));\n\n const entries: TEntry[] = [];\n for (const id of orderedIds) {\n const item = byId.get(id);\n if (!item) continue;\n entries.push(hooks.buildEntry(id, item));\n }\n\n return { entries, total: entries.length };\n}\n\n/**\n * List data-entity records in a workspace via GSI1.\n *\n * `mode` (default `\"full\"`) selects the read shape — see `dispatchListMode`. The data-plane\n * binding here adds the four-shard fan-out (per ADR-011) and the BatchGet hydration with\n * decompression for `mode === \"full\"`. K-way merge by `gsi1sk` is intentionally NOT done here\n * — full server-side natural sort lands with the FHIR list-endpoint plumbing that adds\n * pagination tokens.\n */\nexport async function listDataEntitiesByWorkspace<T>(\n entity: DataEntityWithListAndBatchGet,\n tenantId: string,\n workspaceId: string,\n mode: ListOperationMode = \"full\",\n): Promise<ListResult<T>> {\n const shardResults = await Promise.all(\n Array.from({ length: SHARD_COUNT }, (_, shard) =>\n entity.query\n .gsi1({ tenantId, workspaceId, gsi1Shard: String(shard) })\n .go(),\n ),\n );\n\n return dispatchListMode<{ id: string; resource: string }, ListEntry<T>>(\n mode,\n shardResults,\n {\n hydrate: (orderedIds) =>\n batchGetWithRetry(\n entity,\n orderedIds.map((id) => ({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })),\n ),\n getId: (item) => item.id,\n buildEntry: (id, item) => {\n const parsed = JSON.parse(decompressResource(item.resource)) as T & {\n id?: string;\n };\n return { id, resource: { ...parsed, id } as T };\n },\n buildSummaryEntry: (id, parsed) => ({\n id,\n resource: { ...parsed, id } as T,\n }),\n },\n );\n}\n\n/**\n * Create a data-entity record with put. Computes vid from lastUpdated (from resource meta or fallback).\n * Use from create operations (e.g. Practitioner, Encounter) that build the resource with audit in meta.\n */\nexport async function createDataEntityRecord<T>(\n entity: EntityWithPut,\n tenantId: string,\n workspaceId: string,\n id: string,\n resourceWithAudit: T & { meta?: { lastUpdated?: string } },\n fallbackDate: string,\n): Promise<SingleResourceResult<T>> {\n const lastUpdated =\n resourceWithAudit.meta?.lastUpdated ??\n fallbackDate ??\n new Date().toISOString();\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const resourceLike = resourceWithAudit as unknown as FhirResourceLike;\n const summary = JSON.stringify(extractSummary(resourceLike));\n const gsi1sk = extractSortKey(resourceLike);\n\n await entity\n .put({\n sk: DATA_ENTITY_SK,\n tenantId,\n workspaceId,\n id,\n resource: compressResource(JSON.stringify(resourceWithAudit)),\n summary,\n vid,\n lastUpdated,\n gsi1sk,\n })\n .go();\n\n return {\n id,\n resource: resourceWithAudit as T,\n };\n}\n\n/**\n * Build an updated resource with audit in meta for use with updateDataEntityById.\n * Parses existing resource string for existing meta, merges body with id/resourceType/meta (versionId \"2\"),\n * then merges modified audit (modifiedDate, modifiedById, modifiedByName) into meta.\n * Use from update operations (Patient, Encounter, Practitioner) to avoid duplicating this logic.\n */\nexport function buildUpdatedResourceWithAudit<T extends { meta?: Meta }>(\n body: T,\n id: string,\n date: string,\n actorId: string,\n actorName: string,\n existingResourceStr: string,\n resourceType: string,\n): {\n resource: T & { id: string; meta: MetaWithExtensions };\n lastUpdated: string;\n} {\n const existingMeta: MetaWithExtensions | undefined = (\n JSON.parse(existingResourceStr) as { meta?: MetaWithExtensions }\n ).meta;\n\n const bodyWithMeta = body as T & { id?: string; meta?: Meta };\n const resourceWithVersion: T & { id: string; meta?: Meta } = {\n ...body,\n resourceType: resourceType as T[\"resourceType\"],\n id,\n meta: {\n ...(bodyWithMeta.meta ?? {}),\n lastUpdated: date,\n versionId: \"2\",\n },\n };\n\n const resourceWithAudit: T & { id: string; meta: MetaWithExtensions } = {\n ...resourceWithVersion,\n meta: mergeAuditIntoMeta(resourceWithVersion.meta ?? existingMeta, {\n modifiedDate: date,\n modifiedById: actorId,\n modifiedByName: actorName,\n }),\n };\n\n return {\n resource: resourceWithAudit,\n lastUpdated: date,\n };\n}\n\n/**\n * Update a data-entity record by id: get existing, throw if not found, then call builder with\n * decompressed existing resource string; builder returns \\{ resource, lastUpdated \\}; then patch.\n * Use from update operations with the appropriate entity and resource type.\n */\nexport async function updateDataEntityById<T>(\n entity: EntityWithPatch,\n tenantId: string,\n workspaceId: string,\n id: string,\n resourceLabel: string,\n context: OpenHiContext,\n buildPatched: (existingResourceStr: string) => {\n resource: unknown;\n lastUpdated: string;\n },\n): Promise<SingleResourceResult<T>> {\n const existing = await entity\n .get({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (!existing.data) {\n throw new NotFoundError(`${resourceLabel} ${id} not found`, {\n details: { id },\n });\n }\n\n const existingStr = decompressResource(existing.data.resource);\n const { resource, lastUpdated } = buildPatched(existingStr);\n\n const resourceLike = resource as FhirResourceLike;\n const summary = JSON.stringify(extractSummary(resourceLike));\n const gsi1sk = extractSortKey(resourceLike);\n\n await entity\n .patch({\n tenantId,\n workspaceId,\n id,\n sk: DATA_ENTITY_SK,\n })\n .set({\n resource: compressResource(JSON.stringify(resource)),\n summary,\n lastUpdated,\n gsi1sk,\n })\n .go();\n\n return {\n id,\n resource: resource as T,\n };\n}\n","import { gzipSync, gunzipSync } from \"node:zlib\";\n\n/**\n * @see sites/www-docs/content/packages/@openhi/constructs/lib/compression.md\n */\n\n/** Envelope format version. See ADR 2026-02-15-02 (data layer compression). */\nconst ENVELOPE_VERSION = 1;\n\n/**\n * Compression algorithm identifiers supported by the envelope (string values).\n * Only algos that Node.js supports out of the box (zlib): gzip, brotli, deflate.\n * \"none\" = uncompressed payload. zstd was considered in the ADR but requires native addon/WASM.\n */\nexport const COMPRESSION_ALGOS = {\n NONE: \"none\",\n GZIP: \"gzip\",\n BROTLI: \"brotli\",\n DEFLATE: \"deflate\",\n} as const;\n\n/** Algorithm value for envelope `algo`; only gzip and none are implemented today. */\nexport type CompressionAlgo =\n (typeof COMPRESSION_ALGOS)[keyof typeof COMPRESSION_ALGOS];\n\n/** Stored value is a JSON string of this envelope. */\ninterface CompressionEnvelope {\n v: number;\n algo: string;\n payload: string;\n}\n\nfunction isEnvelope(obj: unknown): obj is CompressionEnvelope {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n \"v\" in obj &&\n \"algo\" in obj &&\n \"payload\" in obj &&\n typeof (obj as CompressionEnvelope).payload === \"string\"\n );\n}\n\n/**\n * Compresses a JSON string (e.g. serialized FHIR resource) for storage in DynamoDB.\n * Uses a versioned envelope: \\{ v, algo, payload \\} with gzip+base64 in payload.\n * Used by the data layer on write; see REST API docs (compression in data layer).\n * Optional compression: pass `{ algo: COMPRESSION_ALGOS.NONE }` to store in envelope without compressing.\n */\nexport function compressResource(\n jsonString: string,\n options?: { algo?: CompressionAlgo },\n): string {\n const algo = options?.algo ?? COMPRESSION_ALGOS.GZIP;\n if (algo === COMPRESSION_ALGOS.NONE) {\n const envelope: CompressionEnvelope = {\n v: ENVELOPE_VERSION,\n algo: COMPRESSION_ALGOS.NONE,\n payload: jsonString,\n };\n return JSON.stringify(envelope);\n }\n const buf = Buffer.from(jsonString, \"utf-8\");\n const payload = gzipSync(buf).toString(\"base64\");\n const envelope: CompressionEnvelope = {\n v: ENVELOPE_VERSION,\n algo: COMPRESSION_ALGOS.GZIP,\n payload,\n };\n return JSON.stringify(envelope);\n}\n\n/**\n * Decompresses a stored value: versioned envelope (v, algo, payload) or legacy gzip+base64 / raw.\n * If the value is not valid envelope JSON, falls back to legacy: try gzip magic on base64, else return as-is.\n */\nexport function decompressResource(compressedOrRaw: string): string {\n try {\n const parsed = JSON.parse(compressedOrRaw) as unknown;\n if (isEnvelope(parsed)) {\n if (parsed.algo === COMPRESSION_ALGOS.GZIP) {\n const buf = Buffer.from(parsed.payload, \"base64\");\n return gunzipSync(buf).toString(\"utf-8\");\n }\n if (parsed.algo === COMPRESSION_ALGOS.NONE) {\n return parsed.payload;\n }\n // Unknown algo: return payload as-is (safe fallback per ADR)\n return parsed.payload;\n }\n } catch {\n // Not valid envelope JSON — legacy path\n }\n\n // Legacy: pre-envelope gzip+base64 or raw\n try {\n const buf = Buffer.from(compressedOrRaw, \"base64\");\n if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) {\n return gunzipSync(buf).toString(\"utf-8\");\n }\n } catch {\n // not base64 or gunzip failed\n }\n return compressedOrRaw;\n}\n"],"mappings":"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","names":["envelope","entries"]}