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,404 @@
1
+ /**
2
+ * The analytics plugin's resource graph. It owns the AWS resources the
3
+ * CloudFront-logs-to-Iceberg pipeline is built from and nothing else: the
4
+ * site's own bucket, distribution and log group stay in the CLI's graph
5
+ * (`packages/cli/src/nodes.ts`) and are never touched from here. This module
6
+ * carries all twelve of them, in four chains. The table chain - the S3 Tables bucket,
7
+ * the namespace inside it, the `page_views` table, and the Glue federation
8
+ * Firehose reads that table through - runs `analytics-table-bucket` ->
9
+ * `analytics-namespace` -> `analytics-table` ->
10
+ * `analytics-catalog-integration`. The transform chain - the long-lived
11
+ * `visitor_key` salt, the Lambda execution role whose policy names that
12
+ * secret's ARN, and the record-transform function itself - runs
13
+ * `analytics-salt-secret` -> `analytics-transform-role` ->
14
+ * `analytics-transform-function`. The delivery chain - the bucket every record
15
+ * Firehose cannot deliver lands in, the role it assumes, and the stream itself
16
+ * - runs `analytics-error-bucket` -> `analytics-firehose-role` ->
17
+ * `analytics-firehose-stream`, and joins the other two chains through the
18
+ * role's four grants and the stream's destination. The vended-delivery chain -
19
+ * the CloudWatch delivery destination pointing at that stream and the delivery
20
+ * joining it to the site's log source - runs `analytics-log-destination` ->
21
+ * `analytics-log-delivery` and hangs off the stream. All four are wired through
22
+ * `dependsOn`, and a node depends on every node whose recorded ARN it
23
+ * interpolates. {@link buildAnalyticsNodes} at the foot of this module returns
24
+ * the assembled set, and `plugin.ts` hands it to the SPI's `Plugin.nodes`;
25
+ * assembling an array is all it does - nothing here reconciles anything.
26
+ *
27
+ * **The delivery source the last chain hangs off is the site's, and this module
28
+ * only ever reads it.** AWS permits exactly one delivery source per
29
+ * distribution, so the site's CloudWatch delivery and this plugin's Firehose
30
+ * delivery necessarily share one, and `packages/cli/src/nodes.ts`'s
31
+ * `logDeliveryNode` owns it. Nothing here calls `putDeliverySource` or
32
+ * `deleteDeliverySource`; the source's name is read off `ctx.names` and the
33
+ * evidence that the site has been bootstrapped off `ctx.siteState`, the SPI's
34
+ * read-only view of the site's state.
35
+ *
36
+ * **Everything in this graph is created in `us-east-1`, whatever
37
+ * `config.region` says.** CloudFront standard logging accepts a Firehose
38
+ * delivery stream only in that region, so the whole pipeline - and therefore
39
+ * the table the stream writes into - has to live there too. The pin is
40
+ * enforced in exactly one place, `aws/clients.ts`, which builds every client
41
+ * over the host's `signingUsEast1` signer; no node here picks a region for a
42
+ * request. {@link ANALYTICS_REGION} below is the same region as *text*, needed
43
+ * only because an ARN spells its region out and because every node `title`
44
+ * states the pin, so the bootstrap output an operator reads carries it. Ten of
45
+ * the twelve titles state it as the region they are created in; the two IAM
46
+ * role nodes state it as the pipeline they serve, because IAM is global and
47
+ * "created in us-east-1" is not a property a role has (§Region pinning says so
48
+ * in as many words) - a title claiming otherwise would be the pin stated
49
+ * falsely rather than stated.
50
+ *
51
+ * The nodes are core's {@link ResourceNode} over {@link PluginContext}, so the
52
+ * CLI's own engine (`topoSort`/`applyGraph`/`destroyGraph`,
53
+ * `packages/cli/src/graph.ts`) reconciles them unchanged - this package
54
+ * contributes nodes, never a second engine.
55
+ *
56
+ * **Creating the namespace and the table through the S3 Tables control-plane
57
+ * API is a supported Firehose source.** Verified 2026-07-26 against AWS's S3
58
+ * Tables + Firehose walkthrough, which creates both with `aws s3tables
59
+ * create-namespace` and `aws s3tables create-table` and then points the
60
+ * delivery stream at them. This is worth recording because Firehose's
61
+ * considerations page carries a limitation that reads as if it forbids exactly
62
+ * that - "only tables created through Iceberg's GlueCatalog API". That
63
+ * limitation applies to plain Iceberg-on-S3 tables registered in Glue, not to
64
+ * S3 Tables reached through the `s3tablescatalog` federation
65
+ * (`analytics-catalog-integration`), which is how this pipeline reaches them.
66
+ * Without this note the next reader re-litigates it.
67
+ */
68
+ import { type PluginContext, type ResourceNode } from 'blogwright-core';
69
+ import { type AnalyticsConfig } from './config.js';
70
+ /**
71
+ * The context every node in this module runs against: core's `PluginContext`
72
+ * narrowed to this plugin's own validated config block, which is what makes
73
+ * {@link resolveAnalyticsConfig} reachable from a node. `ResourceNode`'s own
74
+ * parameter is unconstrained (see core's `plugin.ts`), and the SPI's
75
+ * `Plugin.nodes` returns the bare `ResourceNode[]`; a node typed here is
76
+ * assignable to that, because `read`/`create`/`delete` are method-declared and
77
+ * therefore bivariant in their parameter type.
78
+ */
79
+ type AnalyticsContext = PluginContext<AnalyticsConfig>;
80
+ /** One node of the analytics graph. */
81
+ type AnalyticsNode = ResourceNode<AnalyticsContext>;
82
+ /**
83
+ * The `analytics-firehose-stream` node id. Exported, alone among the twelve,
84
+ * because `analytics status` reads this node's recorded outputs back out of
85
+ * the scoped state its `read` hydrated - the stream's delivery health - and a
86
+ * second copy of the string in `commands.ts` would be a state key with two
87
+ * homes.
88
+ */
89
+ export declare const FIREHOSE_STREAM_NODE = "analytics-firehose-stream";
90
+ /**
91
+ * The `analytics-log-delivery` node id. Exported for `backfill.ts`, which
92
+ * reads {@link CREATED_DAY_KEY} out of this node's recorded outputs: the
93
+ * backfill's idempotency bound and the node that writes it must name the same
94
+ * state entry, and a second spelling of the id is the one way that could stop
95
+ * being true without anything noticing.
96
+ */
97
+ export declare const LOG_DELIVERY_NODE = "analytics-log-delivery";
98
+ /** The S3 Tables bucket every analytics table lives in. */
99
+ export declare function analyticsTableBucketNode(): AnalyticsNode;
100
+ /** The Iceberg namespace inside the table bucket. */
101
+ export declare function analyticsNamespaceNode(): AnalyticsNode;
102
+ /** The `page_views` table, carrying the schema and partition `schema.ts` owns. */
103
+ export declare function analyticsTableNode(): AnalyticsNode;
104
+ /**
105
+ * The Glue `s3tablescatalog` federation Firehose reads the `page_views` table
106
+ * through - **the one node in this graph that adopts shared state rather than
107
+ * owning it.**
108
+ *
109
+ * The integration is account-and-region scoped: a single catalog federates
110
+ * every S3 Tables bucket in the account and Region (see
111
+ * {@link federationSource}), so staging, production and anything else in the
112
+ * account that enabled the S3 Tables integration all read through the same one.
113
+ * Both halves of this node's behaviour follow from that. `read` adopts an
114
+ * existing federation instead of creating a second one, so a second environment
115
+ * converges on what is already there; `delete` removes nothing, so tearing one
116
+ * environment down leaves every other environment's pipeline intact.
117
+ *
118
+ * It depends on `analytics-table` rather than on the bucket even though the
119
+ * federation covers the account rather than any one table. Two reasons: the
120
+ * whole table chain is then in place before anything is federated, so
121
+ * `CreateCatalog`'s own `EntityNotFoundException` - which on that operation
122
+ * means the federated entity is missing, not the catalog - cannot fire merely
123
+ * because the bucket had not been created yet; and `destroyGraph` reverses the
124
+ * order, so this node's `delete` is reached first, before the table it was
125
+ * enabled for is removed.
126
+ */
127
+ export declare function analyticsCatalogIntegrationNode(): AnalyticsNode;
128
+ /**
129
+ * The Secrets Manager secret holding the seed every `visitor_key` in the table
130
+ * is derived from - **the one resource in this graph that must outlive its own
131
+ * reconcile, and the one this module never overwrites or deletes.**
132
+ *
133
+ * `transform/visitor-key.ts` derives the per-day salt as
134
+ * `HMAC-SHA256(secret, day)` and `map-record.ts` hashes the viewer's address
135
+ * under it, so the stored value is the only thing standing between a row in
136
+ * `page_views` and the address it came from - the table keeps `user_agent` in
137
+ * the clear beside the key, and an unsalted SHA-256 over IPv4's 2^32 space is a
138
+ * lookup table, not a pseudonym. Two consequences shape every method below.
139
+ *
140
+ * **It is created once and never rewritten.** Replacing the value does not
141
+ * "rotate" anything: it orphans every `visitor_key` already written, because no
142
+ * row from before the change ever compares equal to a row from after it. The
143
+ * dashboard's unique-visitor figures would silently double at the boundary and
144
+ * `analytics backfill` - which re-derives a historical day's salt from this
145
+ * same seed - would produce rows that join to nothing. So `read` adopts, and
146
+ * `create` re-checks and adopts rather than trusting that the `read` before it
147
+ * is still true (see below). Daily turnover comes from the *derivation*, not
148
+ * from the store, which is why **no Secrets Manager rotation is configured**:
149
+ * managed rotation would mean a rotation Lambda, a schedule and a second
150
+ * execution role, to replace the one value that must not change.
151
+ *
152
+ * **Its `delete` removes nothing.** This is the second inert `delete` in the
153
+ * graph and it is inert for a different reason than
154
+ * `analytics-catalog-integration`'s, which is inert because the federation is
155
+ * shared. This one is inert because the act is asymmetric. Core's
156
+ * `deleteSecret` sends `ForceDeleteWithoutRecovery: true`, so there is no
157
+ * recovery window and nothing to undo with; keeping a secret costs cents a
158
+ * month and one command to remove by hand, while deleting one is unrecoverable
159
+ * and destroys the ability to interpret any `page_views` data that outlived the
160
+ * teardown - an exported copy, a table bucket whose own delete failed, or an
161
+ * environment torn down and re-bootstrapped, which is a routine recovery move
162
+ * and would otherwise come back with a different seed and no sign that
163
+ * anything had changed. The teardown says what it kept and how to remove it.
164
+ */
165
+ export declare function analyticsSaltSecretNode(): AnalyticsNode;
166
+ /**
167
+ * The transform Lambda's execution role: permission to write its own logs and
168
+ * to read the one secret it needs, and nothing else.
169
+ *
170
+ * It declares `dependsOn: ['analytics-salt-secret']` because its policy
171
+ * interpolates that node's *recorded* ARN - the implementation notes' rule that
172
+ * "a node depends on every node whose recorded ARN it interpolates" - and
173
+ * {@link requireSaltSecretArn} explains what an undeclared edge would produce.
174
+ */
175
+ export declare function analyticsTransformRoleNode(): AnalyticsNode;
176
+ /** What a reconcile of the deployed transform has to push. Both false is the common case. */
177
+ export interface TransformUpdate {
178
+ /** Send `UpdateFunctionCode` - the source hash moved. */
179
+ readonly code: boolean;
180
+ /** Send `UpdateFunctionConfiguration` - the settings the function runs under moved. */
181
+ readonly configuration: boolean;
182
+ }
183
+ /**
184
+ * Decide what an existing transform function's reconcile has to send, from what
185
+ * is recorded against what the package now holds. Pure, so the decision is
186
+ * testable without the AWS calls - `builderImageAction`'s shape
187
+ * (`packages/cli/src/nodes.ts:311-321`) and its reason.
188
+ *
189
+ * **Two independent comparisons, not one.** An unchanged hash performs no code
190
+ * call, which is the whole point of hashing the *source*: a rebuild on another
191
+ * machine emits different bundle bytes from the same source, and keying the
192
+ * decision on those bytes would redeploy the function on every platform switch
193
+ * (`transform-hash.ts` argues this at length). Everything a code push could
194
+ * need to accompany it is covered by that hash too, because `analytics/src` is
195
+ * one of the hash's inputs - so the module constants above cannot change
196
+ * without moving it.
197
+ *
198
+ * The configuration is compared separately because one of its inputs is *not*
199
+ * in the hash and cannot be: `analytics.saltSecretName` comes from
200
+ * `blogwright.config.json`, which nothing hashes. An operator who repoints it
201
+ * gets a new secret, a role granted on the new ARN, and - without this
202
+ * comparison - a function still reading the old name out of its environment:
203
+ * every batch failing `GetSecretValue`, every record in Firehose's error
204
+ * prefix. That is the same gap `builderImageAction` carries its second `logGroup`
205
+ * limb for.
206
+ *
207
+ * Keeping them separate is also what keeps the common cases to a single call
208
+ * each, which matters more than it looks: Lambda refuses a second update while
209
+ * the first is still settling, so two calls in one reconcile have a window the
210
+ * one-call cases do not.
211
+ */
212
+ export declare function transformUpdate(recorded: {
213
+ sourceHash: string | undefined;
214
+ configuration: string | undefined;
215
+ }, desired: {
216
+ sourceHash: string;
217
+ configuration: string;
218
+ }): TransformUpdate;
219
+ /**
220
+ * The record-transform Lambda: the function Firehose runs over every CloudFront
221
+ * record before it reaches the `page_views` table.
222
+ *
223
+ * Its code is keyed by task 43's hash of the transform's **source**, recorded
224
+ * in the plugin's own state, so identical source never redeploys it - and the
225
+ * key that hash derives (`transformZipKey`) is recorded beside it as the
226
+ * artifact's name, even though the zip travels inline rather than through a
227
+ * bucket (see {@link MAX_INLINE_ZIP_BYTES} for why inline).
228
+ *
229
+ * It depends on `analytics-transform-role`, whose recorded ARN it runs as.
230
+ */
231
+ export declare function analyticsTransformFunctionNode(): AnalyticsNode;
232
+ /**
233
+ * The S3 bucket every record Firehose cannot deliver is written to - **the
234
+ * physical place a silent pipeline failure becomes visible.**
235
+ *
236
+ * Firehose matches incoming JSON keys to the Iceberg column names exactly and
237
+ * *errors* the records that do not match to this bucket rather than dropping
238
+ * them (the change spec quotes the behaviour). So a missing column, a table
239
+ * whose catalog cannot be read, a Glue grant one level too narrow - none of
240
+ * them raise anything an operator sees. They fill this bucket while the
241
+ * dashboard stays empty, and this bucket is the only place the records
242
+ * themselves still exist. Two properties follow.
243
+ *
244
+ * **It is in {@link ANALYTICS_REGION}, with the rest of the pipeline**, created
245
+ * through the plugin's own {@link s3} client rather than `ctx.clients.s3`,
246
+ * which signs in `config.region`.
247
+ *
248
+ * **It is not the site's environment bucket.** The bucket the CLI's own
249
+ * `bucketNode` creates lives in
250
+ * `config.region` and `S3DestinationConfiguration.BucketARN` matches
251
+ * `arn:.*:s3:::[\w\.\-]{1,255}` - an S3 ARN carries no region, so the API can
252
+ * neither express a cross-region bucket nor reject one, and Firehose's
253
+ * cross-region documentation covers only HTTP endpoint destinations. Pointing
254
+ * at the site's bucket would therefore rest the pipeline's one recovery surface
255
+ * on undocumented behaviour, and would put failed-record objects - which carry
256
+ * the raw CloudFront fields, the viewer IP among them, precisely because the
257
+ * transform did not run on them - inside a bucket the site serves from.
258
+ */
259
+ export declare function analyticsErrorBucketNode(): AnalyticsNode;
260
+ /**
261
+ * The role Firehose assumes to read the catalog, write the table, invoke the
262
+ * transform and store what it could not deliver - four grants, four concrete
263
+ * resources, no `*`.
264
+ *
265
+ * It declares `dependsOn` on the three nodes whose recorded ARNs those grants
266
+ * interpolate. `topoSort` drains zero-indegree nodes alphabetically
267
+ * (`packages/cli/src/graph.ts:35-38`), so a role declaring `dependsOn: []`
268
+ * would be reconciled *before* `analytics-transform-function` - `f` sorts before
269
+ * `t` - and the policy would interpolate an unrecorded output: a wrong
270
+ * permission written silently, never an error. `githubOidcRoleNode`
271
+ * (`packages/cli/src/nodes.ts:830`) is the precedent, declaring
272
+ * `cloudfront-distribution` for exactly this reason.
273
+ */
274
+ export declare function analyticsFirehoseRoleNode(): AnalyticsNode;
275
+ /**
276
+ * The delivery stream itself: CloudFront's records in, the transform Lambda in
277
+ * front of the write, the `page_views` Iceberg table out, and the plugin's own
278
+ * error bucket for everything that does not make it.
279
+ *
280
+ * Its four edges are the ones its payload actually reads. The role edge is the
281
+ * spec's own rule - the `IcebergDestinationConfiguration` interpolates the
282
+ * role's recorded ARN - and it also carries `analytics-error-bucket`
283
+ * transitively, completing the `error-bucket -> firehose-role ->
284
+ * firehose-stream` chain. Without the role edge the ordering would survive only
285
+ * on `topoSort`'s alphabetical accident (`…-role` sorts before `…-stream`), the
286
+ * exact coincidence-reliance the spec's implementation notes warn against.
287
+ *
288
+ * **The `AppendOnly` reconcile is written against neither AWS document.** The
289
+ * Firehose considerations page says the flag is settable only with
290
+ * `CreateDeliveryStream`; the `IcebergDestinationUpdate` API reference lists it
291
+ * among the fields `UpdateDestination` accepts. They cannot both be right, and a
292
+ * node written against either alone is a defect whichever one turns out to be.
293
+ * So `update` attempts the in-place update first and falls back to replacing the
294
+ * stream when it is refused - and only when it is refused: the re-read that
295
+ * follows a successful update sits outside that `try`, because failing it is not
296
+ * a rejection and replacing a stream that was updated correctly would be pure
297
+ * loss. The order matters: `UpdateDestination` keeps the stream's ARN, while a
298
+ * replacement gets a new one - so the CloudFront log delivery task 53 builds
299
+ * would have to be repointed, and the records arriving during the gap are lost.
300
+ * Which path ran is in the log line.
301
+ */
302
+ export declare function analyticsFirehoseStreamNode(): AnalyticsNode;
303
+ /**
304
+ * The state key holding the UTC day this plugin's delivery was **first**
305
+ * created - the idempotency bound the change spec's §Backfill of historical
306
+ * logs defines and task 61 reads.
307
+ *
308
+ * Written once and never advanced. Backfill inserts only whole days *strictly
309
+ * before* it, on the reasoning that Firehose received nothing before its
310
+ * delivery existed, so the two paths' row sets are disjoint. The two error
311
+ * directions are not symmetric, which is why the rule is write-once rather
312
+ * than keep-current: a bound that is too *early* loses at most the day at the
313
+ * seam, which the spec states and accepts, while a bound that moved *later*
314
+ * would let backfill insert days Firehose had already delivered and silently
315
+ * double every row in them. So a second reconcile, a re-created delivery and
316
+ * the destination node's Conflict retry all leave it exactly as it was.
317
+ *
318
+ * `read` never writes it either, even though it hydrates the rest of this
319
+ * node's outputs off the live delivery: `DescribeDeliveries` reports no
320
+ * creation date, so a delivery found already attached to a state file that
321
+ * lost this key leaves task 61 with no bound and an actionable refusal. That
322
+ * is the loud direction, and it is preferred to today's date, which would be a
323
+ * bound that moved later.
324
+ *
325
+ * Exported since task 61, which reads it. It was deliberately module-private
326
+ * while nothing consumed it - an exported constant with no consumer is what
327
+ * `pnpm knip` catches - but a private constant restated in its reader is worse
328
+ * than an exported one: the two spellings would have to agree and nothing
329
+ * would check that they did.
330
+ */
331
+ export declare const CREATED_DAY_KEY = "createdDay";
332
+ /**
333
+ * The CloudWatch delivery destination the site's CloudFront records are
334
+ * delivered to - **this plugin's own, alongside the site's and never in place
335
+ * of it.**
336
+ *
337
+ * Its one edge is the node whose recorded ARN it points at. `PutDeliveryDestination`
338
+ * accepts a `destinationResourceArn` for a resource that does not exist yet, so
339
+ * without the edge the destination would be created against `undefined` and the
340
+ * first symptom would be records going nowhere - see {@link requireStreamArn}.
341
+ *
342
+ * The output format is the one thing about a destination that cannot be
343
+ * changed once it exists, which is why `update` replaces rather than mutates
344
+ * and why the configured format is recorded beside the ARN in the first place.
345
+ */
346
+ export declare function analyticsLogDestinationNode(): AnalyticsNode;
347
+ /**
348
+ * The delivery joining the site's existing delivery source to this plugin's
349
+ * destination - **a second delivery on a source the plugin reads, never
350
+ * creates, never repoints and never deletes.**
351
+ *
352
+ * `putDeliverySource` is not called here and must never be. AWS permits one
353
+ * delivery source per distribution and the site's node owns it
354
+ * (`packages/cli/src/nodes.ts`'s `logDeliveryNode`); this node reads its name
355
+ * off `ctx.names` and attaches a second delivery beside the site's CloudWatch
356
+ * one. The site's copy is left with the field list AWS defaults to, which is
357
+ * deliberate and is `schema.ts`'s to explain.
358
+ *
359
+ * There is no `update`. CloudWatch Logs has no `UpdateDelivery`: the record
360
+ * fields a delivery selects are fixed when it is created, exactly as the
361
+ * `page_views` table's schema is fixed when *it* is created
362
+ * (`aws/s3tables.ts`'s `createTable` reconciles no existing schema). Changing
363
+ * the column set is a rebuild of this pipeline in both places, not a
364
+ * reconcile, and pretending otherwise in one of the two would be worse than
365
+ * saying so in both.
366
+ */
367
+ export declare function analyticsLogDeliveryNode(): AnalyticsNode;
368
+ /**
369
+ * The plugin's twelve resource nodes, assembled in the order the change spec's
370
+ * §Analytics pipeline → Resource nodes table lists them. This is what
371
+ * `Plugin.nodes` (`plugin.ts`) hands the CLI's generic `analytics bootstrap`
372
+ * and `analytics destroy` verbs, and it is the whole of what this package
373
+ * contributes to a reconcile: the engine that walks them - `topoSort`,
374
+ * `applyGraph`, `destroyGraph` - is the CLI's own and is never reimplemented
375
+ * here.
376
+ *
377
+ * **The returned order is itself a topological order**, and that is a property
378
+ * of this array rather than a coincidence of the table's layout: every node's
379
+ * `dependsOn` names only nodes that appear EARLIER in it. That is worth stating
380
+ * because it is exactly the witness `topoSort`'s two failure modes are the
381
+ * absence of - a dependency naming a node outside the set, and a cycle - so a
382
+ * test that checks it has proved the set passes `topoSort` without running a
383
+ * second copy of `topoSort` to find out. `applyGraph` sorts the array again
384
+ * regardless and does not rely on the order it arrives in; nothing here may
385
+ * assume the reconcile follows this sequence, only that this sequence is a
386
+ * legal one.
387
+ *
388
+ * **No `ctx` parameter, deliberately.** The SPI declares `nodes?(ctx)` and the
389
+ * CLI calls it with one, so this function is assignable to it as written - a
390
+ * zero-argument function satisfies a one-argument signature. None of the twelve
391
+ * factories needs a context to be *built*: each reads `ctx` inside `read`,
392
+ * `create`, `update` and `delete`, when the reconcile is actually running. A
393
+ * parameter accepted and ignored here would be an unused binding and, worse, a
394
+ * claim that the SET varies with the context - it does not, and `analytics
395
+ * status` and `analytics destroy` both depend on it not doing so. (The plan's
396
+ * task 54 spells this function `buildAnalyticsNodes(ctx)`; the argument is what
397
+ * changed, not the wiring.)
398
+ *
399
+ * A fresh array of fresh nodes on every call, matching `buildNodes`
400
+ * (`packages/cli/src/nodes.ts`): a node object carries no state between
401
+ * reconciles, and two calls in one process must not share one.
402
+ */
403
+ export declare function buildAnalyticsNodes(): AnalyticsNode[];
404
+ export {};