blogwright-analytics 0.3.3

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 (131) hide show
  1. package/README.md +162 -0
  2. package/dist/adapters/duckdb-ingest.d.ts +76 -0
  3. package/dist/adapters/duckdb-ingest.js +173 -0
  4. package/dist/adapters/duckdb-query.d.ts +56 -0
  5. package/dist/adapters/duckdb-query.js +80 -0
  6. package/dist/adapters/duckdb-session.d.ts +168 -0
  7. package/dist/adapters/duckdb-session.js +330 -0
  8. package/dist/app/_app/immutable/assets/0.BTQrrh5B.css +1 -0
  9. package/dist/app/_app/immutable/assets/2.CZSK3rT8.css +1 -0
  10. package/dist/app/_app/immutable/assets/BrushContext.D7c8UPey.css +1 -0
  11. package/dist/app/_app/immutable/assets/ChartAnnotations.CPxIG7Mw.css +1 -0
  12. package/dist/app/_app/immutable/assets/Circle.C5MKzgk2.css +1 -0
  13. package/dist/app/_app/immutable/assets/DefaultTooltip.C5-uctZ7.css +1 -0
  14. package/dist/app/_app/immutable/assets/Group.DV48xipa.css +1 -0
  15. package/dist/app/_app/immutable/assets/Labels.BxZ4NUVz.css +1 -0
  16. package/dist/app/_app/immutable/assets/Legend.CxnrE4Ye.css +1 -0
  17. package/dist/app/_app/immutable/assets/Line.fkmsECm9.css +1 -0
  18. package/dist/app/_app/immutable/assets/Path.CvpwNZ6g.css +1 -0
  19. package/dist/app/_app/immutable/assets/Rect.CtRaGMmQ.css +1 -0
  20. package/dist/app/_app/immutable/assets/Text.j9l35qB0.css +1 -0
  21. package/dist/app/_app/immutable/assets/TransformContext.Bs_HkpAk.css +1 -0
  22. package/dist/app/_app/immutable/assets/Voronoi.ce7atosu.css +1 -0
  23. package/dist/app/_app/immutable/chunks/-aNGNaBT.js +1 -0
  24. package/dist/app/_app/immutable/chunks/6djn-yLs.js +1 -0
  25. package/dist/app/_app/immutable/chunks/B1amyutE.js +1 -0
  26. package/dist/app/_app/immutable/chunks/B3vZDoek.js +1 -0
  27. package/dist/app/_app/immutable/chunks/B5KRA4hC.js +1 -0
  28. package/dist/app/_app/immutable/chunks/BClnVG6H.js +1 -0
  29. package/dist/app/_app/immutable/chunks/BID1NNRh.js +1 -0
  30. package/dist/app/_app/immutable/chunks/BR2LaRms.js +1 -0
  31. package/dist/app/_app/immutable/chunks/Bd1gDe3Y.js +1 -0
  32. package/dist/app/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  33. package/dist/app/_app/immutable/chunks/Bl052uUt.js +1 -0
  34. package/dist/app/_app/immutable/chunks/Bye3lL0c.js +1 -0
  35. package/dist/app/_app/immutable/chunks/C58PZtCD.js +4 -0
  36. package/dist/app/_app/immutable/chunks/CAzydqEO.js +1 -0
  37. package/dist/app/_app/immutable/chunks/CCch3uox.js +1 -0
  38. package/dist/app/_app/immutable/chunks/CIlSMUH9.js +1 -0
  39. package/dist/app/_app/immutable/chunks/CO1vUXfR.js +1 -0
  40. package/dist/app/_app/immutable/chunks/CPbD8C65.js +5 -0
  41. package/dist/app/_app/immutable/chunks/CRTcXoMo.js +1 -0
  42. package/dist/app/_app/immutable/chunks/CjjyIQAO.js +1 -0
  43. package/dist/app/_app/immutable/chunks/CuXAxjvF.js +1 -0
  44. package/dist/app/_app/immutable/chunks/CvyVA_jC.js +1 -0
  45. package/dist/app/_app/immutable/chunks/CxGCFVdy.js +1 -0
  46. package/dist/app/_app/immutable/chunks/D0Ty6LN0.js +1 -0
  47. package/dist/app/_app/immutable/chunks/D2AaQUUW.js +1 -0
  48. package/dist/app/_app/immutable/chunks/D2BnX0Uk.js +3 -0
  49. package/dist/app/_app/immutable/chunks/DJc8C0NK.js +1 -0
  50. package/dist/app/_app/immutable/chunks/DKMlMI4a.js +1 -0
  51. package/dist/app/_app/immutable/chunks/DVXZkpbf.js +1 -0
  52. package/dist/app/_app/immutable/chunks/DVt8ukQ_.js +1 -0
  53. package/dist/app/_app/immutable/chunks/DZPlYdq_.js +1 -0
  54. package/dist/app/_app/immutable/chunks/Db0q5_zr.js +1 -0
  55. package/dist/app/_app/immutable/chunks/Dfvzj6n2.js +1 -0
  56. package/dist/app/_app/immutable/chunks/Dh958be7.js +1 -0
  57. package/dist/app/_app/immutable/chunks/DjKLLdnY.js +15 -0
  58. package/dist/app/_app/immutable/chunks/Doz7YX1W.js +1 -0
  59. package/dist/app/_app/immutable/chunks/DthYhn6Y.js +2 -0
  60. package/dist/app/_app/immutable/chunks/DtuTIrAM.js +1 -0
  61. package/dist/app/_app/immutable/chunks/HclGiUj8.js +1 -0
  62. package/dist/app/_app/immutable/chunks/Hx0TNsV3.js +1 -0
  63. package/dist/app/_app/immutable/chunks/RobXhXPM.js +1 -0
  64. package/dist/app/_app/immutable/chunks/V9ZjaxiY.js +1 -0
  65. package/dist/app/_app/immutable/chunks/Y5urAfNy.js +1 -0
  66. package/dist/app/_app/immutable/chunks/caXkbKD3.js +1 -0
  67. package/dist/app/_app/immutable/chunks/devYm2ud.js +1 -0
  68. package/dist/app/_app/immutable/chunks/mtZWP0zR.js +1 -0
  69. package/dist/app/_app/immutable/chunks/vDgBJUjM.js +1 -0
  70. package/dist/app/_app/immutable/chunks/xIq_fFFM.js +1 -0
  71. package/dist/app/_app/immutable/chunks/xihTtKlq.js +1 -0
  72. package/dist/app/_app/immutable/chunks/z05MoCFz.js +1 -0
  73. package/dist/app/_app/immutable/entry/app.CLAerUAN.js +2 -0
  74. package/dist/app/_app/immutable/entry/start.D3MqnNci.js +1 -0
  75. package/dist/app/_app/immutable/nodes/0.UTMEigHJ.js +1 -0
  76. package/dist/app/_app/immutable/nodes/1.Cn4f11bT.js +1 -0
  77. package/dist/app/_app/immutable/nodes/2.B39cIcr2.js +6 -0
  78. package/dist/app/_app/version.json +1 -0
  79. package/dist/app/index.html +82 -0
  80. package/dist/aws/clients.d.ts +70 -0
  81. package/dist/aws/clients.js +52 -0
  82. package/dist/aws/errors.d.ts +41 -0
  83. package/dist/aws/errors.js +70 -0
  84. package/dist/aws/firehose.d.ts +228 -0
  85. package/dist/aws/firehose.js +347 -0
  86. package/dist/aws/glue.d.ts +103 -0
  87. package/dist/aws/glue.js +225 -0
  88. package/dist/aws/lambda.d.ts +132 -0
  89. package/dist/aws/lambda.js +339 -0
  90. package/dist/aws/s3tables.d.ts +120 -0
  91. package/dist/aws/s3tables.js +281 -0
  92. package/dist/backfill.d.ts +100 -0
  93. package/dist/backfill.js +294 -0
  94. package/dist/commands.d.ts +124 -0
  95. package/dist/commands.js +336 -0
  96. package/dist/config.d.ts +162 -0
  97. package/dist/config.js +317 -0
  98. package/dist/fixture-ingest.d.ts +49 -0
  99. package/dist/fixture-ingest.js +43 -0
  100. package/dist/fixture-query.d.ts +39 -0
  101. package/dist/fixture-query.js +70 -0
  102. package/dist/index.d.ts +35 -0
  103. package/dist/index.js +35 -0
  104. package/dist/nodes.d.ts +404 -0
  105. package/dist/nodes.js +2708 -0
  106. package/dist/paths.d.ts +45 -0
  107. package/dist/paths.js +47 -0
  108. package/dist/plugin.d.ts +102 -0
  109. package/dist/plugin.js +248 -0
  110. package/dist/ports.d.ts +113 -0
  111. package/dist/ports.js +35 -0
  112. package/dist/queries.d.ts +301 -0
  113. package/dist/queries.js +414 -0
  114. package/dist/schema.d.ts +240 -0
  115. package/dist/schema.js +154 -0
  116. package/dist/server.d.ts +150 -0
  117. package/dist/server.js +499 -0
  118. package/dist/transform/bots.d.ts +47 -0
  119. package/dist/transform/bots.js +73 -0
  120. package/dist/transform/handler.d.ts +135 -0
  121. package/dist/transform/handler.js +177 -0
  122. package/dist/transform/map-record.d.ts +110 -0
  123. package/dist/transform/map-record.js +275 -0
  124. package/dist/transform/visitor-key.d.ts +83 -0
  125. package/dist/transform/visitor-key.js +120 -0
  126. package/dist/transform-bundle/index.mjs +21456 -0
  127. package/dist/transform-bundle/transform-manifest.json +4 -0
  128. package/dist/transform-hash.d.ts +135 -0
  129. package/dist/transform-hash.js +186 -0
  130. package/dist/write-transform-manifest.mjs +365 -0
  131. package/package.json +59 -0
@@ -0,0 +1,103 @@
1
+ import { type SigningClient } from 'blogwright-core';
2
+ /**
3
+ * A catalog federation as this plugin reads it, mapped out of Glue's `Catalog` shape
4
+ * so the catalog-integration node decides whether to adopt without re-reading the raw
5
+ * response. `sourceIdentifier` and `connectionName` are what make that decision
6
+ * possible: a catalog of the right name that is not federated at all, or federated
7
+ * somewhere else, is not something to adopt, and both keys are absent in that case.
8
+ */
9
+ export interface CatalogFederation {
10
+ /** The catalog's name (`s3tablescatalog` for this plugin's federation). */
11
+ readonly name: string;
12
+ /**
13
+ * The catalog's own ARN (`Catalog.ResourceArn`), in the
14
+ * `arn:aws:glue:<region>:<account-id>:catalog/<name>` form. Empty string when the
15
+ * service reports none.
16
+ */
17
+ readonly resourceArn: string;
18
+ /**
19
+ * `FederatedCatalog.Identifier` - the S3 Tables resource the catalog federates, as
20
+ * `arn:aws:s3tables:<region>:<account-id>:bucket/*`. Undefined when the catalog
21
+ * exists but carries no `FederatedCatalog` at all.
22
+ */
23
+ readonly sourceIdentifier: string | undefined;
24
+ /** `FederatedCatalog.ConnectionName` - `aws:s3tables` for this federation. Undefined when the catalog is not federated. */
25
+ readonly connectionName: string | undefined;
26
+ }
27
+ /**
28
+ * AWS Glue Data Catalog client for the S3 Tables federation, over the shared SigV4
29
+ * transport.
30
+ *
31
+ * Both narrowings below use `AwsError`'s predicates unmodified, unlike this module's
32
+ * two siblings, and that is a property of the service rather than luck: Glue is
33
+ * AWS-JSON, so its error body carries `{"__type":"EntityNotFoundException",...}` and
34
+ * core's `parseError` (`packages/core/src/aws/signer.ts`) reads a real exception name
35
+ * into `AwsError.code` - where `s3tables.ts` gets `Http<status>` because S3 Tables
36
+ * puts the name in an `x-amzn-ErrorType` header the parser never reads. And Glue's
37
+ * names happen to fall inside both patterns: `EntityNotFoundException` matches
38
+ * `isNotFound`'s `/NotFound/i` (`packages/core/src/aws/errors.ts:24`), while both
39
+ * `AlreadyExistsException` *and* `FederatedResourceAlreadyExistsException` - the two
40
+ * distinct duplicates `CreateCatalog` documents - match `isAlreadyExists`'s
41
+ * `/AlreadyExists/i` (`errors.ts:32`). So no local predicate is needed here and core's
42
+ * regex, which the site's own bootstrap shares, is left alone.
43
+ *
44
+ * `statusCode` is no help either way and is never narrowed on: every Glue exception is
45
+ * HTTP 400 except `InternalServiceException` at 500, so `isNotFound`'s `=== 404` limb
46
+ * and any `=== 409` limb are dead on this service - the code is the only signal. Glue
47
+ * also returns its request id only in the `x-amzn-requestid` header, which
48
+ * `parseError` does not read, so `AwsError.requestId` is always `undefined` here.
49
+ */
50
+ export declare class GlueClient {
51
+ private readonly client;
52
+ constructor(client: SigningClient);
53
+ private call;
54
+ /**
55
+ * Create the `s3tablescatalog` federation over an S3 Tables bucket resource.
56
+ * `tableBucketArn` is the `FederatedCatalog.Identifier`, and AWS's integration
57
+ * procedure passes the account-and-region wildcard
58
+ * `arn:aws:s3tables:<region>:<account-id>:bucket/*` there: one catalog federates
59
+ * every table bucket in the account and Region, mounting each as a child catalog.
60
+ * This client sends whatever the caller passes rather than building the wildcard
61
+ * itself - the node owns the account id and Region.
62
+ *
63
+ * Idempotent: an already-existing federation resolves rather than throwing, because
64
+ * the integration is **account-and-region-scoped shared state**. Two blogwright
65
+ * environments in one account share the single `s3tablescatalog`, as does anything
66
+ * else in that account that enabled the integration, so a second environment's
67
+ * bootstrap must adopt what is there instead of fighting over it. Both duplicate
68
+ * exceptions are covered: `AlreadyExistsException` when a catalog of that name
69
+ * exists, and `FederatedResourceAlreadyExistsException` when the federation itself
70
+ * is already registered (which is what a second run against a console-enabled
71
+ * account hits).
72
+ *
73
+ * Nothing else is swallowed. In particular `EntityNotFoundException`, which
74
+ * `CreateCatalog` also documents, means something entirely different here than it
75
+ * does on `getCatalogFederation` - not "no such catalog" but "the entity this
76
+ * catalog would federate does not exist", i.e. the S3 Tables resource in
77
+ * `tableBucketArn` is wrong. Treating that as success would report a federation that
78
+ * was never created.
79
+ *
80
+ * Returns `void`: `CreateCatalog`'s success response is an empty body, so there is
81
+ * nothing to return, and a caller that needs the catalog's ARN reads it back with
82
+ * `getCatalogFederation` - which is the call it made before creating anyway.
83
+ */
84
+ createCatalogFederation(name: string, tableBucketArn: string): Promise<void>;
85
+ /**
86
+ * Look the federation up by catalog name; `undefined` when it does not exist, so the
87
+ * catalog-integration node adopts an existing federation instead of creating one, in
88
+ * the `packages/core/src/aws/secretsmanager.ts:78-89` shape.
89
+ *
90
+ * The name *is* the id: `GetCatalog` is keyed by `CatalogId`, and for a catalog
91
+ * created directly under the account that id is the catalog's own name (AWS's
92
+ * verification step for the integration is literally
93
+ * `aws glue get-catalog --catalog-id s3tablescatalog`). It takes no bucket ARN -
94
+ * `GetCatalogRequest` has exactly one member.
95
+ *
96
+ * Only `EntityNotFoundException` reads as absent. `FederationSourceException` in
97
+ * particular is rethrown even though its `FederationSourceErrorCode` can itself be
98
+ * `EntityNotFoundException`: `parseError` reads the outer `__type`, so `code` is the
99
+ * wrapper name, and a federation source that failed is a broken federation to
100
+ * surface, not a missing one to silently re-create.
101
+ */
102
+ getCatalogFederation(name: string): Promise<CatalogFederation | undefined>;
103
+ }
@@ -0,0 +1,225 @@
1
+ import { AwsError } from 'blogwright-core';
2
+ import { rethrowWithContext } from './errors.js';
3
+ /**
4
+ * AWS Glue Data Catalog client, cut down to the one thing this plugin needs: the
5
+ * `s3tablescatalog` federation Firehose writes the `page_views` table through.
6
+ * Firehose reaches an S3 Tables table only via a Glue catalog
7
+ * (`IcebergDestinationConfiguration.CatalogConfiguration.CatalogARN`), never via
8
+ * the S3 Tables API directly, so without this federation the delivery stream has
9
+ * nothing to point at. It exposes exactly two operations - create the federation
10
+ * and look one up - because `analytics-catalog-integration` is a read-then-adopt
11
+ * node and needs nothing else; no update, no delete (the change spec makes that
12
+ * node's `delete()` a no-op, since the federation is account-and-region scoped
13
+ * shared state that other environments and other tools also depend on).
14
+ *
15
+ * It lives in `blogwright-analytics`, not in core: core's `SIGNING_NAMES` gains no
16
+ * `glue` key, and every request signs through the `{ service: 'glue', signingName:
17
+ * 'glue' }` descriptor the plugin transport seam accepts (see
18
+ * `packages/core/src/aws/endpoint.ts`'s `ServiceDescriptor`), which resolves to the
19
+ * canonical `glue.<region>.amazonaws.com` host.
20
+ *
21
+ * Protocol: AWS JSON 1.1 - `POST /` with `content-type: application/x-amz-json-1.1`
22
+ * and an `x-amz-target: AWSGlue.<Operation>` header, exactly as
23
+ * `packages/core/src/aws/secretsmanager.ts` and `logs.ts` do for their services.
24
+ * `AWSGlue` is the service's own target prefix (`glue-2017-03-31`'s `targetPrefix`),
25
+ * and both operations are `POST /` despite Glue's reference calling them "Get" and
26
+ * "Create".
27
+ *
28
+ * Operation names and body keys below are verified against the Glue API reference
29
+ * (`CreateCatalog`, `GetCatalog`, and the `CatalogInput`, `FederatedCatalog`,
30
+ * `PrincipalPermissions` and `Catalog` shapes they nest) and against AWS's own
31
+ * documented S3 Tables integration procedure. No SDK validates them here and a
32
+ * transport-mocked test can only assert the body this module itself builds, so the
33
+ * reference is the only thing that catches a wrong or missing key - and here a wrong
34
+ * key produces a *silently misconfigured federation*, not an error: `CatalogInput`
35
+ * has no required members at all, so a `CreateCatalog` that omitted
36
+ * `FederatedCatalog` entirely would return HTTP 200 and leave an empty non-federated
37
+ * catalog behind under the right name, which `getCatalogFederation` would then
38
+ * happily adopt.
39
+ *
40
+ * Two easy-to-miss spellings are pinned deliberately:
41
+ * - `Name` is a *sibling* of `CatalogInput`, not a member of it. `CatalogInput`
42
+ * carries no name; putting one inside it is silently ignored.
43
+ * - `CatalogNameString`'s pattern is `^(?!(.*[.\/\\]|aws:)).*$` - a catalog name may
44
+ * not start with `aws:`. That restriction does *not* apply to
45
+ * `FederatedCatalog.ConnectionName`, which is exactly `aws:s3tables`; the two are
46
+ * different fields with different patterns.
47
+ *
48
+ * The floci emulator does not implement this service, so it is covered by transport
49
+ * mocks in tests.
50
+ */
51
+ const SERVICE = { service: 'glue', signingName: 'glue' };
52
+ /** The service's AWS-JSON target prefix; every `x-amz-target` is `${TARGET}.<Operation>`. */
53
+ const TARGET = 'AWSGlue';
54
+ /**
55
+ * The AWS-managed connection that federates S3 Tables into the Data Catalog, sent as
56
+ * `FederatedCatalog.ConnectionName`. Hard-coded rather than a parameter because this
57
+ * client exists only for the S3 Tables federation; any other connection would make
58
+ * `createCatalogFederation` a different operation. `FederatedCatalog.ConnectionType`
59
+ * is deliberately not sent - AWS's documented S3 Tables integration omits it, and the
60
+ * connection name alone identifies the source.
61
+ */
62
+ const S3_TABLES_CONNECTION = 'aws:s3tables';
63
+ /**
64
+ * The default permissions that put the federated catalog under **IAM access control**
65
+ * rather than Lake Formation grants, sent as both
66
+ * `CatalogInput.CreateDatabaseDefaultPermissions` and `CreateTableDefaultPermissions`.
67
+ *
68
+ * Load-bearing, not decoration. `CatalogInput` marks both fields optional, so omitting
69
+ * them succeeds - and leaves the catalog under Lake Formation control, where every
70
+ * read and write needs a grant this plugin never provisions. Firehose's delivery role
71
+ * would be denied on write and the change spec's whole "requires `s3tables`
72
+ * permissions but no Lake Formation grant" assumption would fail, with the only
73
+ * symptom a stream quietly routing every record to the error bucket. AWS's own S3
74
+ * Tables integration procedure sets both to `IAM_ALLOWED_PRINCIPALS` with `ALL` for
75
+ * exactly this reason.
76
+ *
77
+ * `AllowFullTableExternalDataAccess` is deliberately *not* sent alongside them. It
78
+ * only opens Lake-Formation-registered S3 locations to third-party engines, and the
79
+ * one third-party reader here (DuckDB) attaches through S3 Tables' own endpoint, not
80
+ * through Glue credential vending - so setting it would widen access this pipeline
81
+ * never uses. Note if it is ever needed that it is a **string** enum, `"True"` or
82
+ * `"False"`, not a boolean.
83
+ */
84
+ const IAM_DEFAULT_PERMISSIONS = [
85
+ { Principal: { DataLakePrincipalIdentifier: 'IAM_ALLOWED_PRINCIPALS' }, Permissions: ['ALL'] },
86
+ ];
87
+ /**
88
+ * Map `GetCatalog`'s response onto the domain value.
89
+ *
90
+ * A 200 is the existence signal: `GetCatalogResponse.Catalog` is optional in the
91
+ * service model, but a 200 carrying no catalog is neither documented nor observed, so
92
+ * an absent field falls back rather than inventing a second "does not exist" answer
93
+ * next to `EntityNotFoundException` - the same call this module's siblings make
94
+ * (`s3tables.ts`'s `normalizeTable`, `firehose.ts`'s `describeDeliveryStream`).
95
+ */
96
+ function normalizeCatalog(out, fallbackName) {
97
+ const catalog = out.Catalog;
98
+ return {
99
+ name: catalog?.Name ?? fallbackName,
100
+ resourceArn: catalog?.ResourceArn ?? '',
101
+ sourceIdentifier: catalog?.FederatedCatalog?.Identifier,
102
+ connectionName: catalog?.FederatedCatalog?.ConnectionName,
103
+ };
104
+ }
105
+ /**
106
+ * AWS Glue Data Catalog client for the S3 Tables federation, over the shared SigV4
107
+ * transport.
108
+ *
109
+ * Both narrowings below use `AwsError`'s predicates unmodified, unlike this module's
110
+ * two siblings, and that is a property of the service rather than luck: Glue is
111
+ * AWS-JSON, so its error body carries `{"__type":"EntityNotFoundException",...}` and
112
+ * core's `parseError` (`packages/core/src/aws/signer.ts`) reads a real exception name
113
+ * into `AwsError.code` - where `s3tables.ts` gets `Http<status>` because S3 Tables
114
+ * puts the name in an `x-amzn-ErrorType` header the parser never reads. And Glue's
115
+ * names happen to fall inside both patterns: `EntityNotFoundException` matches
116
+ * `isNotFound`'s `/NotFound/i` (`packages/core/src/aws/errors.ts:24`), while both
117
+ * `AlreadyExistsException` *and* `FederatedResourceAlreadyExistsException` - the two
118
+ * distinct duplicates `CreateCatalog` documents - match `isAlreadyExists`'s
119
+ * `/AlreadyExists/i` (`errors.ts:32`). So no local predicate is needed here and core's
120
+ * regex, which the site's own bootstrap shares, is left alone.
121
+ *
122
+ * `statusCode` is no help either way and is never narrowed on: every Glue exception is
123
+ * HTTP 400 except `InternalServiceException` at 500, so `isNotFound`'s `=== 404` limb
124
+ * and any `=== 409` limb are dead on this service - the code is the only signal. Glue
125
+ * also returns its request id only in the `x-amzn-requestid` header, which
126
+ * `parseError` does not read, so `AwsError.requestId` is always `undefined` here.
127
+ */
128
+ export class GlueClient {
129
+ client;
130
+ constructor(client) {
131
+ this.client = client;
132
+ }
133
+ async call(op, payload) {
134
+ const res = await this.client.send({
135
+ service: SERVICE,
136
+ method: 'POST',
137
+ path: '/',
138
+ headers: {
139
+ 'content-type': 'application/x-amz-json-1.1',
140
+ 'x-amz-target': `${TARGET}.${op}`,
141
+ },
142
+ body: JSON.stringify(payload),
143
+ });
144
+ const text = res.text();
145
+ return (text ? JSON.parse(text) : {});
146
+ }
147
+ /**
148
+ * Create the `s3tablescatalog` federation over an S3 Tables bucket resource.
149
+ * `tableBucketArn` is the `FederatedCatalog.Identifier`, and AWS's integration
150
+ * procedure passes the account-and-region wildcard
151
+ * `arn:aws:s3tables:<region>:<account-id>:bucket/*` there: one catalog federates
152
+ * every table bucket in the account and Region, mounting each as a child catalog.
153
+ * This client sends whatever the caller passes rather than building the wildcard
154
+ * itself - the node owns the account id and Region.
155
+ *
156
+ * Idempotent: an already-existing federation resolves rather than throwing, because
157
+ * the integration is **account-and-region-scoped shared state**. Two blogwright
158
+ * environments in one account share the single `s3tablescatalog`, as does anything
159
+ * else in that account that enabled the integration, so a second environment's
160
+ * bootstrap must adopt what is there instead of fighting over it. Both duplicate
161
+ * exceptions are covered: `AlreadyExistsException` when a catalog of that name
162
+ * exists, and `FederatedResourceAlreadyExistsException` when the federation itself
163
+ * is already registered (which is what a second run against a console-enabled
164
+ * account hits).
165
+ *
166
+ * Nothing else is swallowed. In particular `EntityNotFoundException`, which
167
+ * `CreateCatalog` also documents, means something entirely different here than it
168
+ * does on `getCatalogFederation` - not "no such catalog" but "the entity this
169
+ * catalog would federate does not exist", i.e. the S3 Tables resource in
170
+ * `tableBucketArn` is wrong. Treating that as success would report a federation that
171
+ * was never created.
172
+ *
173
+ * Returns `void`: `CreateCatalog`'s success response is an empty body, so there is
174
+ * nothing to return, and a caller that needs the catalog's ARN reads it back with
175
+ * `getCatalogFederation` - which is the call it made before creating anyway.
176
+ */
177
+ async createCatalogFederation(name, tableBucketArn) {
178
+ try {
179
+ await this.call('CreateCatalog', {
180
+ Name: name,
181
+ CatalogInput: {
182
+ FederatedCatalog: {
183
+ Identifier: tableBucketArn,
184
+ ConnectionName: S3_TABLES_CONNECTION,
185
+ },
186
+ CreateDatabaseDefaultPermissions: IAM_DEFAULT_PERMISSIONS,
187
+ CreateTableDefaultPermissions: IAM_DEFAULT_PERMISSIONS,
188
+ },
189
+ });
190
+ }
191
+ catch (err) {
192
+ if (err instanceof AwsError && err.isAlreadyExists)
193
+ return;
194
+ rethrowWithContext(err, 'createCatalogFederation', name);
195
+ }
196
+ }
197
+ /**
198
+ * Look the federation up by catalog name; `undefined` when it does not exist, so the
199
+ * catalog-integration node adopts an existing federation instead of creating one, in
200
+ * the `packages/core/src/aws/secretsmanager.ts:78-89` shape.
201
+ *
202
+ * The name *is* the id: `GetCatalog` is keyed by `CatalogId`, and for a catalog
203
+ * created directly under the account that id is the catalog's own name (AWS's
204
+ * verification step for the integration is literally
205
+ * `aws glue get-catalog --catalog-id s3tablescatalog`). It takes no bucket ARN -
206
+ * `GetCatalogRequest` has exactly one member.
207
+ *
208
+ * Only `EntityNotFoundException` reads as absent. `FederationSourceException` in
209
+ * particular is rethrown even though its `FederationSourceErrorCode` can itself be
210
+ * `EntityNotFoundException`: `parseError` reads the outer `__type`, so `code` is the
211
+ * wrapper name, and a federation source that failed is a broken federation to
212
+ * surface, not a missing one to silently re-create.
213
+ */
214
+ async getCatalogFederation(name) {
215
+ try {
216
+ const out = await this.call('GetCatalog', { CatalogId: name });
217
+ return normalizeCatalog(out, name);
218
+ }
219
+ catch (err) {
220
+ if (err instanceof AwsError && err.isNotFound)
221
+ return undefined;
222
+ rethrowWithContext(err, 'getCatalogFederation', name);
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,132 @@
1
+ import { type SigningClient } from 'blogwright-core';
2
+ /**
3
+ * The version-specific settings shared by `CreateFunction` and
4
+ * `UpdateFunctionConfiguration` - the two operations that carry exactly these keys,
5
+ * one nested in a create body and one as the whole body of the configuration PUT.
6
+ * Expressed once so the pair cannot drift.
7
+ */
8
+ export interface FunctionConfigurationInput {
9
+ /** ARN of the function's execution role (`Role`). */
10
+ readonly roleArn: string;
11
+ /** Runtime identifier, e.g. `nodejs22.x` (`Runtime`). */
12
+ readonly runtime: string;
13
+ /** Entry point, `<file>.<export>` for a Node runtime (`Handler`). */
14
+ readonly handler: string;
15
+ /** Memory available to the function in **MB** (`MemorySize`; 128-32768, any multiple of 1 MB). */
16
+ readonly memoryMb: number;
17
+ /** Maximum run time in **seconds** (`Timeout`; the service caps it at 900). */
18
+ readonly timeoutSeconds: number;
19
+ /**
20
+ * Environment variables (`Environment.Variables`). Absent and empty mean different
21
+ * things to the service on an update and are sent differently: omitting the key
22
+ * leaves the function's existing variables untouched, while an explicit map
23
+ * replaces them wholesale - so an empty map *clears* them.
24
+ */
25
+ readonly environment?: Record<string, string> | undefined;
26
+ }
27
+ /** Everything `CreateFunction` needs on top of the shared configuration. */
28
+ export interface CreateFunctionInput extends FunctionConfigurationInput {
29
+ /** The function's name (`FunctionName`), also the name every other operation addresses it by. */
30
+ readonly name: string;
31
+ /**
32
+ * The deployment package as raw zip bytes, sent inline. This client base64-encodes
33
+ * them into the wire's blob member itself - that translation is its concern, not
34
+ * its callers'. Inline rather than an S3 code location because Lambda requires the
35
+ * code bucket to sit in the function's own region while the site's bucket is in
36
+ * `config.region` and this function is pinned to us-east-1; the node that reads the
37
+ * bundle owns the size guard against the inline-payload limit.
38
+ */
39
+ readonly zipFile: Uint8Array;
40
+ }
41
+ /**
42
+ * A function's lifecycle state in this repo's vocabulary rather than the service's
43
+ * mixed case, mirroring how `firehose.ts` maps `DeliveryStreamStatus`. It is not
44
+ * decoration: a function that exists but is `failed` is not something the
45
+ * transform-function node should adopt as reconciled (the pipeline would then route
46
+ * every record to the error bucket with nothing surfacing as an error), and an update
47
+ * against a `pending` function is answered with a 409. `unknown` covers a state the
48
+ * service adds later - reporting an unrecognised state as unknown is honest, where
49
+ * mapping it onto one of the eight would not be.
50
+ */
51
+ export type FunctionState = 'pending' | 'active' | 'inactive' | 'failed' | 'deactivating' | 'deactivated' | 'active-non-invocable' | 'deleting' | 'unknown';
52
+ /** The narrow view of `GetFunction` the transform-function node needs. */
53
+ export interface LambdaFunction {
54
+ readonly name: string;
55
+ /** The function's ARN (`Configuration.FunctionArn`). Empty string when the service reports none. */
56
+ readonly arn: string;
57
+ readonly state: FunctionState;
58
+ }
59
+ /** AWS Lambda function client, over the shared SigV4 transport. */
60
+ export declare class LambdaClient {
61
+ private readonly client;
62
+ constructor(client: SigningClient);
63
+ private call;
64
+ /**
65
+ * Create the function with its code and configuration in one request. Idempotent:
66
+ * a function of the same name already existing is not an error (see
67
+ * `isFunctionAlreadyExists`). Its code and configuration are *not* reconciled
68
+ * against `input` on that path - that is what `updateFunctionCode` and
69
+ * `updateFunctionConfiguration` are for, and the node that owns the function
70
+ * decides between them on its recorded source hash.
71
+ *
72
+ * A 404 is deliberately **not** swallowed here even though `getFunction` and
73
+ * `deleteFunction` read one as absence. On this operation
74
+ * `ResourceNotFoundException` does not mean "no such function" - the function is
75
+ * the thing being created - it means the execution role in `roleArn` does not
76
+ * exist. Treating it as success would report a function that was never created,
77
+ * the same asymmetry `glue.ts`'s `createCatalogFederation` documents for
78
+ * `EntityNotFoundException`.
79
+ *
80
+ * Returns `void`, discarding the response's `FunctionArn`: it is unavailable on the
81
+ * already-exists path (the error body carries no ARN), so returning it from only
82
+ * one of the two branches would be a false economy. A caller needing the ARN either
83
+ * reads it back with `getFunction` - the call it made before creating anyway - or
84
+ * builds the fixed `arn:aws:lambda:<region>:<account-id>:function:<name>` form from
85
+ * what a `PluginContext` already carries, exactly as `s3tables.ts`'s
86
+ * `createTableBucket` argues.
87
+ *
88
+ * Tags are not accepted. The transform-function node does not tag the function, and
89
+ * tagging it later is `TagResource`, a separate operation this client can grow when
90
+ * a node needs it, rather than a parameter carried unused today.
91
+ */
92
+ createFunction(input: CreateFunctionInput): Promise<void>;
93
+ /**
94
+ * The function's name, ARN and lifecycle state; `undefined` when no such function
95
+ * exists, so the transform-function node's `read` reports absence without throwing
96
+ * and its `create` runs instead. Lambda answers an absent function with
97
+ * `ResourceNotFoundException` at HTTP 404, which `AwsError.isNotFound` matches on
98
+ * its status limb.
99
+ */
100
+ getFunction(name: string): Promise<LambdaFunction | undefined>;
101
+ /**
102
+ * Replace the function's deployment package. The zip bytes go on the wire as a
103
+ * **top-level** `ZipFile`, not nested under `Code` the way `CreateFunction` takes
104
+ * them - the two operations do not share a body shape.
105
+ *
106
+ * Nothing is swallowed. A 409 here means a previous update has not finished, not
107
+ * that the code is already what was sent, and a 404 means the function is gone -
108
+ * both are the node's to see.
109
+ */
110
+ updateFunctionCode(name: string, zipFile: Uint8Array): Promise<void>;
111
+ /**
112
+ * Replace the function's version-specific settings - role, runtime, handler, memory,
113
+ * timeout and environment. The name travels in the path, never in the body, which
114
+ * is why this takes it as a separate argument rather than reusing
115
+ * `CreateFunctionInput`.
116
+ *
117
+ * Nothing is swallowed, for the same reasons as `updateFunctionCode`.
118
+ */
119
+ updateFunctionConfiguration(name: string, input: FunctionConfigurationInput): Promise<void>;
120
+ /**
121
+ * Delete the function and every version and alias of it. No-op when it does not
122
+ * exist, so teardown is re-runnable.
123
+ *
124
+ * Every other failure is rethrown with context - including the 409
125
+ * `ResourceConflictException`, which on this operation means the function is
126
+ * mid-operation and cannot be deleted yet, not that it is already gone. Swallowing
127
+ * it (by reusing `isFunctionAlreadyExists` here) would report a live function as
128
+ * torn down, leaving the Firehose stream pointing at a processor the teardown
129
+ * claimed to have removed.
130
+ */
131
+ deleteFunction(name: string): Promise<void>;
132
+ }