@reventlessdev/reventless-aws 3.0.0-alpha.239 → 3.0.0-alpha.241

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.
@@ -0,0 +1,136 @@
1
+ // How a stack lays out the object stores its plugins declare, and how hard it
2
+ // protects them. Pure functions so both decisions are decidable and unit-
3
+ // testable without touching Pulumi — the same shape `Util_HostUiDomain` uses
4
+ // for the host-shell FQDN.
5
+ //
6
+ // Two decisions, deliberately independent:
7
+ //
8
+ // layout — one bucket per store, or one bucket per stack with `{store}/…`
9
+ // prefixes inside it
10
+ // protection — whether an accidental removal may destroy the store
11
+ //
12
+ // It is tempting to make one switch decide both, and that is wrong. `alpha`
13
+ // shares a bucket *and* holds hand-entered data worth protecting; a `pr-*`
14
+ // stack shares a bucket and must be destroyable or teardown leaks exactly the
15
+ // buckets sharing exists to save. Layout answers "how many buckets"; protection
16
+ // answers "is this data disposable". Those are different questions about the
17
+ // same stack.
18
+ //
19
+ // Why production alone gets per-store buckets: the growth term is
20
+ // `plugins × stores × stacks`, and stacks is the factor that actually grows —
21
+ // the stack allowlist admits `pr-*`, so per-PR environments multiply every
22
+ // plugin's every store against a 100-bucket account default. Production does
23
+ // not multiply. It is also the only environment where a bucket boundary (rather
24
+ // than a prefix boundary) is worth its cost, since the honest price of sharing
25
+ // is that per-store least-privilege degrades from a bucket ARN to a prefix ARN.
26
+
27
+ /** How many buckets a stack's declared stores get. */
28
+ type t =
29
+ | PerStore
30
+ | SharedBucket
31
+
32
+ /** Whether a store may be destroyed by an accidental removal. */
33
+ type protection =
34
+ | Protected
35
+ | Unprotected
36
+
37
+ /** Which distribution fronts the declared stores. */
38
+ type serving =
39
+ /** No store is declared, so nothing is served. */
40
+ | NoStores
41
+ /** The host shell's own distribution serves them same-origin, so a minted
42
+ `/{prefix}/…` ref resolves relative and there is no base URL. */
43
+ | HostShell
44
+ /** The platform fronts them itself, because no host shell is deployed. */
45
+ | PlatformOwned
46
+
47
+ /**
48
+ Stack-name prefixes whose stacks are disposable.
49
+
50
+ `pr-*` matches the stack allowlist's per-PR environments. A prefix rather than
51
+ an exact list because the whole point of these stacks is that nobody enumerates
52
+ them in advance.
53
+ */
54
+ let defaultEphemeralPrefixes = ["pr-"]
55
+
56
+ /**
57
+ Production gets a bucket per store; every other stack shares one.
58
+
59
+ `prodStacks` comes from `Util_HostUiDomain.resolveProdStacks` — the same notion
60
+ of "production" that names the host-shell domain, on purpose.
61
+
62
+ Note this polarity **fails open**: a new production stack whose name is not on
63
+ the list (`production`, `live`, `prod-eu`) silently gets the weaker layout and
64
+ nothing errors. That is the accepted cost of keying off a name allowlist, paid
65
+ down by the list being config-overridable and by the deploy logging the layout
66
+ it chose — a silent fail-open is only dangerous while it is silent.
67
+ */
68
+ let layoutFor = (~stack: string, ~prodStacks: array<string>): t =>
69
+ prodStacks->Array.includes(stack) ? PerStore : SharedBucket
70
+
71
+ /**
72
+ Destroy semantics follow disposability, **not** layout.
73
+
74
+ `alpha` shares a bucket and is still protected: it declares
75
+ `reventless:wipeable`, but that authorises the reset tool to empty stores
76
+ *deliberately*, after a scope prompt and a typed confirmation. It does not say a
77
+ field rename may destroy a bucket by accident. Only stacks that are routinely
78
+ torn down are unprotected.
79
+ */
80
+ let protectionFor = (~stack: string, ~ephemeralPrefixes: array<string>=defaultEphemeralPrefixes): protection =>
81
+ ephemeralPrefixes->Array.some(p => stack->String.startsWith(p)) ? Unprotected : Protected
82
+
83
+ /**
84
+ The bucket a store's objects live in.
85
+
86
+ Per-store: `{plugin}-{store}`, so the physical name traces back to the
87
+ declaration that required it — a store rename becomes a visible replace in
88
+ review rather than a silent one. Shared: one `{stack}-stores` bucket for every
89
+ declared store on the stack.
90
+
91
+ Pulumi appends its own suffix for global uniqueness, so neither form has to
92
+ carry an account or region discriminator.
93
+ */
94
+ let bucketNameFor = (~layout: t, ~stack: string, ~plugin: string, ~store: string): string =>
95
+ switch layout {
96
+ | PerStore => `${plugin}-${store}`
97
+ | SharedBucket => `${stack}-stores`
98
+ }
99
+
100
+ /**
101
+ The prefix a store's object keys are rooted at — the store name, in **both**
102
+ layouts.
103
+
104
+ This is the property that keeps the two layouts one model rather than a fork.
105
+ The presign service mints `{store}/{identity}{uuid}/{file}` either way, so the
106
+ stored ref is `/{store}/…` regardless of which bucket sits behind it, and only
107
+ the CDN origin differs. Refs live in an append-only event log: one that encoded
108
+ its bucket layout would be environment-specific and unrewritable, so a prod dump
109
+ restored into a PR stack would carry refs that cannot resolve.
110
+
111
+ The cost is a slightly redundant prefix inside a dedicated bucket
112
+ (`catalog-productImages/productImages/…`). Take the redundancy.
113
+ */
114
+ let keyPrefixFor = (~store: string): string => store
115
+
116
+ /**
117
+ Who serves the declared stores — and the answer is never "both".
118
+
119
+ A store's bucket blocks public policy and takes its read grant solely from a
120
+ distribution's `BucketPolicy`. **S3 permits exactly one bucket policy per
121
+ bucket**, so two distributions fronting one store would each write that single
122
+ policy and silently unpick the other's grant: green deploy, 404s afterwards.
123
+ Making this one function's return a three-way choice is what keeps "both" from
124
+ being expressible.
125
+
126
+ The polarity is the useful part. Provisioning a store and serving it are
127
+ separate; before this, serving happened only as a side car to a host-UI bundle,
128
+ so a platform whose UI shipped from its own stack provisioned stores that
129
+ nothing could read.
130
+ */
131
+ let servingFor = (~hasHostUiBundle: bool, ~declaredBucketCount: int): serving =>
132
+ switch (hasHostUiBundle, declaredBucketCount) {
133
+ | (_, 0) => NoStores
134
+ | (true, _) => HostShell
135
+ | (false, _) => PlatformOwned
136
+ }
@@ -0,0 +1,55 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let defaultEphemeralPrefixes = ["pr-"];
5
+
6
+ function layoutFor(stack, prodStacks) {
7
+ if (prodStacks.includes(stack)) {
8
+ return "PerStore";
9
+ } else {
10
+ return "SharedBucket";
11
+ }
12
+ }
13
+
14
+ function protectionFor(stack, ephemeralPrefixesOpt) {
15
+ let ephemeralPrefixes = ephemeralPrefixesOpt !== undefined ? ephemeralPrefixesOpt : defaultEphemeralPrefixes;
16
+ if (ephemeralPrefixes.some(p => stack.startsWith(p))) {
17
+ return "Unprotected";
18
+ } else {
19
+ return "Protected";
20
+ }
21
+ }
22
+
23
+ function bucketNameFor(layout, stack, plugin, store) {
24
+ if (layout === "PerStore") {
25
+ return plugin + `-` + store;
26
+ } else {
27
+ return stack + `-stores`;
28
+ }
29
+ }
30
+
31
+ function keyPrefixFor(store) {
32
+ return store;
33
+ }
34
+
35
+ function servingFor(hasHostUiBundle, declaredBucketCount) {
36
+ if (declaredBucketCount !== 0) {
37
+ if (hasHostUiBundle) {
38
+ return "HostShell";
39
+ } else {
40
+ return "PlatformOwned";
41
+ }
42
+ } else {
43
+ return "NoStores";
44
+ }
45
+ }
46
+
47
+ export {
48
+ defaultEphemeralPrefixes,
49
+ layoutFor,
50
+ protectionFor,
51
+ bucketNameFor,
52
+ keyPrefixFor,
53
+ servingFor,
54
+ }
55
+ /* No side effect */
@@ -0,0 +1,155 @@
1
+ open JestGlobals
2
+
3
+ let prodStacks = Util_HostUiDomain.defaultProdStacks
4
+
5
+ describe("Util_StoreLayout.layoutFor", () => {
6
+ testSync("a prod-named stack gets a bucket per store", () =>
7
+ expect(Util_StoreLayout.layoutFor(~stack="prod", ~prodStacks))->toEqual(Util_StoreLayout.PerStore)
8
+ )
9
+
10
+ testSync("'main' is production too, by the shared prod list", () =>
11
+ expect(Util_StoreLayout.layoutFor(~stack="main", ~prodStacks))->toEqual(Util_StoreLayout.PerStore)
12
+ )
13
+
14
+ testSync("alpha shares one bucket", () =>
15
+ expect(Util_StoreLayout.layoutFor(~stack="alpha", ~prodStacks))->toEqual(
16
+ Util_StoreLayout.SharedBucket,
17
+ )
18
+ )
19
+
20
+ testSync("a PR stack shares one bucket — the factor that multiplies", () =>
21
+ expect(Util_StoreLayout.layoutFor(~stack="pr-1234", ~prodStacks))->toEqual(
22
+ Util_StoreLayout.SharedBucket,
23
+ )
24
+ )
25
+
26
+ // The accepted fail-open: a production stack whose name is not on the list
27
+ // gets the weaker layout silently. Asserted so the behaviour is a decision on
28
+ // record rather than a surprise, and so the config override is the documented
29
+ // fix rather than a code change.
30
+ testSync("an unlisted production name falls open to the shared layout", () =>
31
+ expect(Util_StoreLayout.layoutFor(~stack="production", ~prodStacks))->toEqual(
32
+ Util_StoreLayout.SharedBucket,
33
+ )
34
+ )
35
+
36
+ testSync("adding it to the prod list is the fix", () =>
37
+ expect(Util_StoreLayout.layoutFor(~stack="production", ~prodStacks=["prod", "production"]))->toEqual(
38
+ Util_StoreLayout.PerStore,
39
+ )
40
+ )
41
+ })
42
+
43
+ describe("Util_StoreLayout.servingFor", () => {
44
+ testSync("no declared store means nothing to serve", () =>
45
+ expect(Util_StoreLayout.servingFor(~hasHostUiBundle=false, ~declaredBucketCount=0))->toEqual(
46
+ Util_StoreLayout.NoStores,
47
+ )
48
+ )
49
+
50
+ testSync("a host shell serves the stores from its own origin", () =>
51
+ expect(Util_StoreLayout.servingFor(~hasHostUiBundle=true, ~declaredBucketCount=1))->toEqual(
52
+ Util_StoreLayout.HostShell,
53
+ )
54
+ )
55
+
56
+ // The case that was previously unrepresentable and produced a write-only
57
+ // store: stores are provisioned unconditionally, but serving used to happen
58
+ // only as a side car to a host-UI bundle.
59
+ testSync("no host shell means the platform serves them itself", () =>
60
+ expect(Util_StoreLayout.servingFor(~hasHostUiBundle=false, ~declaredBucketCount=1))->toEqual(
61
+ Util_StoreLayout.PlatformOwned,
62
+ )
63
+ )
64
+
65
+ // A bucket carries exactly one policy, so two distributions fronting one
66
+ // store would unpick each other's read grant. The three-way return is what
67
+ // makes "both" unrepresentable — asserted so it stays that way.
68
+ testSync("a host shell wins even with several buckets — never both", () =>
69
+ expect(Util_StoreLayout.servingFor(~hasHostUiBundle=true, ~declaredBucketCount=3))->toEqual(
70
+ Util_StoreLayout.HostShell,
71
+ )
72
+ )
73
+
74
+ // Declaring nothing outranks having a shell: with no store there is no
75
+ // bucket, no policy and no origin, whoever is deployed.
76
+ testSync("no store outranks a host shell", () =>
77
+ expect(Util_StoreLayout.servingFor(~hasHostUiBundle=true, ~declaredBucketCount=0))->toEqual(
78
+ Util_StoreLayout.NoStores,
79
+ )
80
+ )
81
+ })
82
+
83
+ describe("Util_StoreLayout.protectionFor", () => {
84
+ // The pairing a single layout-driven switch would have got wrong: alpha
85
+ // shares a bucket and is still protected.
86
+ testSync("alpha shares a bucket and is still protected", () => {
87
+ expect(Util_StoreLayout.layoutFor(~stack="alpha", ~prodStacks))->toEqual(
88
+ Util_StoreLayout.SharedBucket,
89
+ )
90
+ expect(Util_StoreLayout.protectionFor(~stack="alpha"))->toEqual(Util_StoreLayout.Protected)
91
+ })
92
+
93
+ testSync("prod is protected", () =>
94
+ expect(Util_StoreLayout.protectionFor(~stack="prod"))->toEqual(Util_StoreLayout.Protected)
95
+ )
96
+
97
+ testSync("a PR stack is unprotected so teardown does not leak buckets", () =>
98
+ expect(Util_StoreLayout.protectionFor(~stack="pr-42"))->toEqual(Util_StoreLayout.Unprotected)
99
+ )
100
+
101
+ testSync("'prepare' is not a PR stack — the prefix is 'pr-', not 'pr'", () =>
102
+ expect(Util_StoreLayout.protectionFor(~stack="prepare"))->toEqual(Util_StoreLayout.Protected)
103
+ )
104
+ })
105
+
106
+ describe("Util_StoreLayout.bucketNameFor", () => {
107
+ testSync("per-store names the bucket after the declaration", () =>
108
+ expect(
109
+ Util_StoreLayout.bucketNameFor(
110
+ ~layout=PerStore,
111
+ ~stack="prod",
112
+ ~plugin="catalog",
113
+ ~store="productImages",
114
+ ),
115
+ )->toBe("catalog-productImages")
116
+ )
117
+
118
+ testSync("shared names one bucket per stack", () =>
119
+ expect(
120
+ Util_StoreLayout.bucketNameFor(
121
+ ~layout=SharedBucket,
122
+ ~stack="alpha",
123
+ ~plugin="catalog",
124
+ ~store="productImages",
125
+ ),
126
+ )->toBe("alpha-stores")
127
+ )
128
+
129
+ testSync("every store on a shared stack lands in the same bucket", () =>
130
+ expect(
131
+ Util_StoreLayout.bucketNameFor(
132
+ ~layout=SharedBucket,
133
+ ~stack="alpha",
134
+ ~plugin="ordering",
135
+ ~store="invoices",
136
+ ),
137
+ )->toBe(
138
+ Util_StoreLayout.bucketNameFor(
139
+ ~layout=SharedBucket,
140
+ ~stack="alpha",
141
+ ~plugin="catalog",
142
+ ~store="productImages",
143
+ ),
144
+ )
145
+ )
146
+ })
147
+
148
+ describe("Util_StoreLayout.keyPrefixFor", () => {
149
+ // The assertion the whole dual-layout scheme rests on: a ref is
150
+ // layout-independent, so the same declaration produces the same stored string
151
+ // on a per-store stack and a shared one.
152
+ testSync("the key prefix is the store name in both layouts", () =>
153
+ expect(Util_StoreLayout.keyPrefixFor(~store="productImages"))->toBe("productImages")
154
+ )
155
+ })
@@ -0,0 +1,87 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Util_StoreLayout$ReventlessAws from "../src/util/Util_StoreLayout.res.mjs";
4
+ import * as Util_HostUiDomain$ReventlessAws from "../src/util/Util_HostUiDomain.res.mjs";
5
+
6
+ globalThis.describe("Util_StoreLayout.layoutFor", () => {
7
+ globalThis.test("a prod-named stack gets a bucket per store", () => {
8
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("prod", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("PerStore");
9
+ });
10
+ globalThis.test("'main' is production too, by the shared prod list", () => {
11
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("main", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("PerStore");
12
+ });
13
+ globalThis.test("alpha shares one bucket", () => {
14
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("alpha", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
15
+ });
16
+ globalThis.test("a PR stack shares one bucket — the factor that multiplies", () => {
17
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("pr-1234", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
18
+ });
19
+ globalThis.test("an unlisted production name falls open to the shared layout", () => {
20
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("production", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
21
+ });
22
+ globalThis.test("adding it to the prod list is the fix", () => {
23
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("production", [
24
+ "prod",
25
+ "production"
26
+ ])).toEqual("PerStore");
27
+ });
28
+ });
29
+
30
+ globalThis.describe("Util_StoreLayout.servingFor", () => {
31
+ globalThis.test("no declared store means nothing to serve", () => {
32
+ globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(false, 0)).toEqual("NoStores");
33
+ });
34
+ globalThis.test("a host shell serves the stores from its own origin", () => {
35
+ globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(true, 1)).toEqual("HostShell");
36
+ });
37
+ globalThis.test("no host shell means the platform serves them itself", () => {
38
+ globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(false, 1)).toEqual("PlatformOwned");
39
+ });
40
+ globalThis.test("a host shell wins even with several buckets — never both", () => {
41
+ globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(true, 3)).toEqual("HostShell");
42
+ });
43
+ globalThis.test("no store outranks a host shell", () => {
44
+ globalThis.expect(Util_StoreLayout$ReventlessAws.servingFor(true, 0)).toEqual("NoStores");
45
+ });
46
+ });
47
+
48
+ globalThis.describe("Util_StoreLayout.protectionFor", () => {
49
+ globalThis.test("alpha shares a bucket and is still protected", () => {
50
+ globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("alpha", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
51
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("alpha", undefined)).toEqual("Protected");
52
+ });
53
+ globalThis.test("prod is protected", () => {
54
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("prod", undefined)).toEqual("Protected");
55
+ });
56
+ globalThis.test("a PR stack is unprotected so teardown does not leak buckets", () => {
57
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("pr-42", undefined)).toEqual("Unprotected");
58
+ });
59
+ globalThis.test("'prepare' is not a PR stack — the prefix is 'pr-', not 'pr'", () => {
60
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("prepare", undefined)).toEqual("Protected");
61
+ });
62
+ });
63
+
64
+ globalThis.describe("Util_StoreLayout.bucketNameFor", () => {
65
+ globalThis.test("per-store names the bucket after the declaration", () => {
66
+ globalThis.expect(Util_StoreLayout$ReventlessAws.bucketNameFor("PerStore", "prod", "catalog", "productImages")).toBe("catalog-productImages");
67
+ });
68
+ globalThis.test("shared names one bucket per stack", () => {
69
+ globalThis.expect(Util_StoreLayout$ReventlessAws.bucketNameFor("SharedBucket", "alpha", "catalog", "productImages")).toBe("alpha-stores");
70
+ });
71
+ globalThis.test("every store on a shared stack lands in the same bucket", () => {
72
+ globalThis.expect(Util_StoreLayout$ReventlessAws.bucketNameFor("SharedBucket", "alpha", "ordering", "invoices")).toBe(Util_StoreLayout$ReventlessAws.bucketNameFor("SharedBucket", "alpha", "catalog", "productImages"));
73
+ });
74
+ });
75
+
76
+ globalThis.describe("Util_StoreLayout.keyPrefixFor", () => {
77
+ globalThis.test("the key prefix is the store name in both layouts", () => {
78
+ globalThis.expect(Util_StoreLayout$ReventlessAws.keyPrefixFor("productImages")).toBe("productImages");
79
+ });
80
+ });
81
+
82
+ let prodStacks = Util_HostUiDomain$ReventlessAws.defaultProdStacks;
83
+
84
+ export {
85
+ prodStacks,
86
+ }
87
+ /* Not a pure module */