@spacefast/common 0.0.10 → 0.0.12

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 (122) hide show
  1. package/dist/agents/connect-targets.d.ts +4 -4
  2. package/dist/agents/connect-targets.js +3 -3
  3. package/dist/agents/private-key-oauth.d.ts +37 -0
  4. package/dist/agents/private-key-oauth.js +158 -0
  5. package/dist/config/domains.js +1 -1
  6. package/dist/config/index.d.ts +1 -0
  7. package/dist/config/index.js +1 -0
  8. package/dist/config/postgres-budget.d.ts +126 -0
  9. package/dist/config/postgres-budget.js +135 -0
  10. package/dist/config/postgres-budget.json +50 -0
  11. package/dist/contracts/access.d.ts +42 -24
  12. package/dist/contracts/access.js +73 -34
  13. package/dist/contracts/activity.d.ts +1 -1
  14. package/dist/contracts/activity.js +5 -5
  15. package/dist/contracts/api-keys.d.ts +53 -45
  16. package/dist/contracts/api-keys.js +1 -0
  17. package/dist/contracts/archives.d.ts +8 -8
  18. package/dist/contracts/builds.d.ts +8 -8
  19. package/dist/contracts/builds.js +28 -48
  20. package/dist/contracts/channels.js +1 -2
  21. package/dist/contracts/comments.d.ts +55 -19
  22. package/dist/contracts/comments.js +56 -17
  23. package/dist/contracts/common.d.ts +4 -3
  24. package/dist/contracts/common.js +2 -0
  25. package/dist/contracts/deployments.d.ts +3 -3
  26. package/dist/contracts/device-auth.d.ts +14 -12
  27. package/dist/contracts/docs.d.ts +5 -5
  28. package/dist/contracts/domains.d.ts +261 -1330
  29. package/dist/contracts/domains.js +39 -301
  30. package/dist/contracts/email-preferences.d.ts +50 -0
  31. package/dist/contracts/email-preferences.js +44 -0
  32. package/dist/contracts/enums.d.ts +1 -1
  33. package/dist/contracts/error-code-meta.d.ts +84 -15
  34. package/dist/contracts/error-code-meta.js +28 -4
  35. package/dist/contracts/error-codes.d.ts +1 -1
  36. package/dist/contracts/error-codes.js +24 -6
  37. package/dist/contracts/events.d.ts +13 -13
  38. package/dist/contracts/feature-lifecycle.d.ts +644 -2
  39. package/dist/contracts/feature-lifecycle.js +685 -2
  40. package/dist/contracts/features.d.ts +236 -24
  41. package/dist/contracts/features.js +3 -272
  42. package/dist/contracts/ids.d.ts +1 -2
  43. package/dist/contracts/ids.js +1 -2
  44. package/dist/contracts/internal.d.ts +3 -3
  45. package/dist/contracts/mcp.d.ts +6 -6
  46. package/dist/contracts/me.js +1 -1
  47. package/dist/contracts/mounts.d.ts +142 -0
  48. package/dist/contracts/mounts.js +56 -0
  49. package/dist/contracts/oauth-resources.d.ts +5 -5
  50. package/dist/contracts/oauth-resources.js +1 -0
  51. package/dist/contracts/operations.d.ts +2 -2
  52. package/dist/contracts/pages.d.ts +2 -2
  53. package/dist/contracts/pages.js +8 -3
  54. package/dist/contracts/plan-policy.d.ts +1 -1
  55. package/dist/contracts/platform.d.ts +3 -3
  56. package/dist/contracts/resources.d.ts +12 -20
  57. package/dist/contracts/resources.js +13 -37
  58. package/dist/contracts/runtime-api.d.ts +330 -16
  59. package/dist/contracts/runtime-api.js +58 -14
  60. package/dist/contracts/sf-config-v1.js +1 -1
  61. package/dist/contracts/sites.d.ts +12 -12
  62. package/dist/contracts/sites.js +18 -11
  63. package/dist/contracts/space-config.d.ts +2 -3
  64. package/dist/contracts/space-config.js +3 -1
  65. package/dist/contracts/spaces.d.ts +143 -102
  66. package/dist/contracts/spaces.js +109 -43
  67. package/dist/contracts/superadmin-emails.d.ts +25 -1
  68. package/dist/contracts/superadmin-emails.js +10 -15
  69. package/dist/contracts/superadmin-runtime.d.ts +2 -2
  70. package/dist/contracts/superadmin-spaces.d.ts +6 -14
  71. package/dist/contracts/tags.d.ts +43 -13
  72. package/dist/contracts/tags.js +195 -11
  73. package/dist/contracts/transfers.d.ts +1 -1
  74. package/dist/contracts/variables.d.ts +4 -4
  75. package/dist/contracts/variables.js +4 -3
  76. package/dist/contracts/webhooks.d.ts +2 -2
  77. package/dist/contracts/zero.d.ts +329 -7
  78. package/dist/contracts/zero.js +121 -2
  79. package/dist/dashboard-paths/index.d.ts +1 -1
  80. package/dist/docs/agent-setup.d.ts +4 -2
  81. package/dist/docs/agent-setup.js +15 -7
  82. package/dist/docs/agent-solutions.js +1 -1
  83. package/dist/docs/catalog.d.ts +8 -10
  84. package/dist/docs/catalog.js +8 -10
  85. package/dist/docs/error-docs.js +86 -18
  86. package/dist/utils/access-grant.d.ts +14 -0
  87. package/dist/utils/access-grant.js +88 -0
  88. package/dist/utils/access-match.d.ts +7 -0
  89. package/dist/utils/access-match.fixtures.json +513 -0
  90. package/dist/utils/access-match.js +32 -16
  91. package/dist/utils/access-verdict.fixtures.json +421 -0
  92. package/dist/utils/credential-policy.d.ts +1 -1
  93. package/dist/utils/dns-instructions.d.ts +29 -0
  94. package/dist/utils/dns-instructions.js +78 -0
  95. package/dist/utils/domain-diagnostics.d.ts +29 -0
  96. package/dist/utils/domain-diagnostics.js +63 -0
  97. package/dist/utils/egress-policy.fixtures.json +51 -0
  98. package/dist/utils/id-hints.d.ts +1 -0
  99. package/dist/utils/id-hints.js +55 -13
  100. package/dist/utils/page-colors.d.ts +50 -0
  101. package/dist/utils/page-colors.js +167 -0
  102. package/dist/utils/page-fonts.d.ts +2 -0
  103. package/dist/utils/page-fonts.js +4 -0
  104. package/dist/utils/pages.d.ts +8 -1
  105. package/dist/utils/pages.js +123 -37
  106. package/dist/utils/production-runtime.d.ts +18 -0
  107. package/dist/utils/production-runtime.js +28 -0
  108. package/dist/utils/query-keys.d.ts +6 -7
  109. package/dist/utils/query-keys.js +11 -12
  110. package/dist/utils/sf-config-v1.fixtures.json +9 -0
  111. package/dist/utils/sf-config-v1.js +28 -19
  112. package/dist/utils/source-archive-policy.d.ts +2 -0
  113. package/dist/utils/source-archive-policy.js +2 -0
  114. package/dist/utils/static-runtime-policy.fixtures.json +57 -0
  115. package/dist/utils/static-runtime-policy.js +1 -1
  116. package/package.json +5 -4
  117. package/dist/contracts/annotations.d.ts +0 -327
  118. package/dist/contracts/annotations.js +0 -235
  119. package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
  120. package/dist/contracts/generated-feature-launch-entries.js +0 -518
  121. package/dist/contracts/publishes.d.ts +0 -135
  122. package/dist/contracts/publishes.js +0 -162
@@ -0,0 +1,421 @@
1
+ {
2
+ "_comment": "Shared corpus for the simulate<->runtime verdict drift guard (access-plan §8/X-9). Asserted against the PHP pure kernel (_spacefast_evaluate_policy, runtime/engine/runtime/access-rules.php) in apps/control-plane/src/runtime/php-policy-parity.test.ts and against apps/control-plane/src/access/simulate.ts's simulateAccess in apps/control-plane/src/runtime/access-verdict-parity.test.ts. `rules` is a raw RuntimeRule[] (no source/order provenance). `context` carries only the 4 fields simulate.ts's SimulateRequest supports (host/path/ip/country) plus `now` for expiry. `grants` mirrors what a real visitor token would carry but is restricted to 0-or-1 `email:` entries (simulate.ts's own simplification — it only ever mints an `email:` grant); `identified` is `grants.length > 0`, matching simulateAccess. `expected` is the {ruleId, effect, satisfied} verdict tuple both sides must agree on.",
3
+ "verdicts": [
4
+ {
5
+ "name": "first-match wins: anonymous deny before a later allow-all",
6
+ "rules": [
7
+ { "id": "r1", "match": { "pathPattern": "/admin/**" }, "effect": "deny" },
8
+ { "id": "r2", "match": {}, "effect": "allow" }
9
+ ],
10
+ "context": {
11
+ "host": "acme.example.com",
12
+ "path": "/admin/x",
13
+ "ip": "1.2.3.4",
14
+ "country": "US"
15
+ },
16
+ "grants": [],
17
+ "identified": false,
18
+ "now": 1770000000,
19
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
20
+ },
21
+ {
22
+ "name": "first match: satisfied allow wins over a later deny",
23
+ "rules": [
24
+ {
25
+ "id": "r1",
26
+ "match": {},
27
+ "effect": "allow",
28
+ "auth": { "requiredGrants": ["email:carol@acme.com"] }
29
+ },
30
+ { "id": "r2", "match": {}, "effect": "deny" }
31
+ ],
32
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
33
+ "grants": ["email:carol@acme.com"],
34
+ "identified": true,
35
+ "now": 1,
36
+ "expected": { "ruleId": "r1", "effect": "allow", "satisfied": true }
37
+ },
38
+ {
39
+ "name": "expired deny rule is skipped, falls through to public allow",
40
+ "rules": [
41
+ { "id": "r1", "match": {}, "effect": "deny", "expiresAt": 1000 },
42
+ { "id": "r2", "match": {}, "effect": "allow" }
43
+ ],
44
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
45
+ "grants": [],
46
+ "identified": false,
47
+ "now": 2000,
48
+ "expected": { "ruleId": "r2", "effect": "allow", "satisfied": true }
49
+ },
50
+ {
51
+ "name": "not-yet-expired rule (expiresAt > now) still applies",
52
+ "rules": [{ "id": "r1", "match": {}, "effect": "deny", "expiresAt": 2001 }],
53
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
54
+ "grants": [],
55
+ "identified": false,
56
+ "now": 2000,
57
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
58
+ },
59
+ {
60
+ "name": "expiresAt == now is expired at the boundary (falls through to public allow)",
61
+ "rules": [{ "id": "r1", "match": {}, "effect": "deny", "expiresAt": 2000 }],
62
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
63
+ "grants": [],
64
+ "identified": false,
65
+ "now": 2000,
66
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
67
+ },
68
+ {
69
+ "name": "challenge unsatisfied + identified visitor becomes deny (X-32 never re-challenge)",
70
+ "rules": [
71
+ {
72
+ "id": "r1",
73
+ "match": {},
74
+ "effect": "challenge",
75
+ "auth": { "requiredGrants": ["team:t1:member"] }
76
+ }
77
+ ],
78
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
79
+ "grants": ["email:carol@acme.com"],
80
+ "identified": true,
81
+ "now": 1,
82
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": false }
83
+ },
84
+ {
85
+ "name": "challenge unsatisfied + anonymous stays a challenge",
86
+ "rules": [
87
+ {
88
+ "id": "r1",
89
+ "match": {},
90
+ "effect": "challenge",
91
+ "auth": { "requiredGrants": ["team:t1:member"] }
92
+ }
93
+ ],
94
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
95
+ "grants": [],
96
+ "identified": false,
97
+ "now": 1,
98
+ "expected": { "ruleId": "r1", "effect": "challenge", "satisfied": false }
99
+ },
100
+ {
101
+ "name": "challenge satisfied becomes allow",
102
+ "rules": [
103
+ {
104
+ "id": "r1",
105
+ "match": {},
106
+ "effect": "challenge",
107
+ "auth": { "requiredGrants": ["email:carol@acme.com"] }
108
+ }
109
+ ],
110
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
111
+ "grants": ["email:carol@acme.com"],
112
+ "identified": true,
113
+ "now": 1,
114
+ "expected": { "ruleId": "r1", "effect": "allow", "satisfied": true }
115
+ },
116
+ {
117
+ "name": "allow rule unsatisfied keeps scanning to default public allow",
118
+ "rules": [
119
+ {
120
+ "id": "r1",
121
+ "match": {},
122
+ "effect": "allow",
123
+ "auth": { "requiredGrants": ["email:carol@acme.com"] }
124
+ }
125
+ ],
126
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
127
+ "grants": [],
128
+ "identified": false,
129
+ "now": 1,
130
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
131
+ },
132
+ {
133
+ "name": "deny rule unsatisfied keeps scanning to default public allow",
134
+ "rules": [
135
+ {
136
+ "id": "r1",
137
+ "match": {},
138
+ "effect": "deny",
139
+ "auth": { "requiredGrants": ["email:carol@acme.com"] }
140
+ }
141
+ ],
142
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
143
+ "grants": [],
144
+ "identified": false,
145
+ "now": 1,
146
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
147
+ },
148
+ {
149
+ "name": "deny rule satisfied denies immediately",
150
+ "rules": [
151
+ {
152
+ "id": "r1",
153
+ "match": {},
154
+ "effect": "deny",
155
+ "auth": { "requiredGrants": ["email:mallory@acme.com"] }
156
+ }
157
+ ],
158
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
159
+ "grants": ["email:mallory@acme.com"],
160
+ "identified": true,
161
+ "now": 1,
162
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
163
+ },
164
+ {
165
+ "name": "host mismatch falls through to public allow",
166
+ "rules": [{ "id": "r1", "match": { "host": "other.example.com" }, "effect": "deny" }],
167
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
168
+ "grants": [],
169
+ "identified": false,
170
+ "now": 1,
171
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
172
+ },
173
+ {
174
+ "name": "path glob ** matches any depth",
175
+ "rules": [{ "id": "r1", "match": { "pathPattern": "/docs/**" }, "effect": "deny" }],
176
+ "context": { "host": "acme.example.com", "path": "/docs/a/b/c", "ip": "", "country": "" },
177
+ "grants": [],
178
+ "identified": false,
179
+ "now": 1,
180
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
181
+ },
182
+ {
183
+ "name": "path glob * matches one segment only, deeper path falls through",
184
+ "rules": [
185
+ { "id": "r1", "match": { "pathPattern": "/docs/*" }, "effect": "deny" },
186
+ { "id": "r2", "match": {}, "effect": "allow" }
187
+ ],
188
+ "context": { "host": "acme.example.com", "path": "/docs/a/b", "ip": "", "country": "" },
189
+ "grants": [],
190
+ "identified": false,
191
+ "now": 1,
192
+ "expected": { "ruleId": "r2", "effect": "allow", "satisfied": true }
193
+ },
194
+ {
195
+ "name": "country match denies",
196
+ "rules": [{ "id": "r1", "match": { "country": "KP" }, "effect": "deny" }],
197
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "KP" },
198
+ "grants": [],
199
+ "identified": false,
200
+ "now": 1,
201
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
202
+ },
203
+ {
204
+ "name": "country mismatch falls through to public allow",
205
+ "rules": [{ "id": "r1", "match": { "country": "KP" }, "effect": "deny" }],
206
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "US" },
207
+ "grants": [],
208
+ "identified": false,
209
+ "now": 1,
210
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
211
+ },
212
+ {
213
+ "name": "countryNotIn lets an explicitly allowed country fall through",
214
+ "rules": [{ "id": "r1", "match": { "countryNotIn": ["US"] }, "effect": "deny" }],
215
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "US" },
216
+ "grants": [],
217
+ "identified": false,
218
+ "now": 1,
219
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
220
+ },
221
+ {
222
+ "name": "countryNotIn denies a provider-special country",
223
+ "rules": [{ "id": "r1", "match": { "countryNotIn": ["US"] }, "effect": "deny" }],
224
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "XX" },
225
+ "grants": [],
226
+ "identified": false,
227
+ "now": 1,
228
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
229
+ },
230
+ {
231
+ "name": "countryNotIn denies a missing country",
232
+ "rules": [{ "id": "r1", "match": { "countryNotIn": ["US"] }, "effect": "deny" }],
233
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
234
+ "grants": [],
235
+ "identified": false,
236
+ "now": 1,
237
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
238
+ },
239
+ {
240
+ "name": "ip cidr match denies",
241
+ "rules": [{ "id": "r1", "match": { "ipCidrs": ["10.0.0.0/8"] }, "effect": "deny" }],
242
+ "context": { "host": "acme.example.com", "path": "/", "ip": "10.1.2.3", "country": "" },
243
+ "grants": [],
244
+ "identified": false,
245
+ "now": 1,
246
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
247
+ },
248
+ {
249
+ "name": "ip cidr mismatch falls through to public allow",
250
+ "rules": [{ "id": "r1", "match": { "ipCidrs": ["10.0.0.0/8"] }, "effect": "deny" }],
251
+ "context": { "host": "acme.example.com", "path": "/", "ip": "192.168.1.1", "country": "" },
252
+ "grants": [],
253
+ "identified": false,
254
+ "now": 1,
255
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
256
+ },
257
+ {
258
+ "name": "agent-conditioned rule never fires when unsimulated (simulate.ts scope gap, negative-only)",
259
+ "rules": [
260
+ { "id": "r1", "match": { "agent": "Googlebot" }, "effect": "deny" },
261
+ { "id": "r2", "match": {}, "effect": "allow" }
262
+ ],
263
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
264
+ "grants": [],
265
+ "identified": false,
266
+ "now": 1,
267
+ "expected": { "ruleId": "r2", "effect": "allow", "satisfied": true }
268
+ },
269
+ {
270
+ "name": "grant glob intersection: email domain wildcard satisfies",
271
+ "rules": [
272
+ {
273
+ "id": "r1",
274
+ "match": {},
275
+ "effect": "allow",
276
+ "auth": { "requiredGrants": ["email:*@acme.com"] }
277
+ }
278
+ ],
279
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
280
+ "grants": ["email:carol@acme.com"],
281
+ "identified": true,
282
+ "now": 1,
283
+ "expected": { "ruleId": "r1", "effect": "allow", "satisfied": true }
284
+ },
285
+ {
286
+ "name": "grant glob intersection: email outside the domain wildcard does not satisfy",
287
+ "rules": [
288
+ {
289
+ "id": "r1",
290
+ "match": {},
291
+ "effect": "allow",
292
+ "auth": { "requiredGrants": ["email:*@acme.com"] }
293
+ },
294
+ { "id": "r2", "match": {}, "effect": "deny" }
295
+ ],
296
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
297
+ "grants": ["email:carol@evil.com"],
298
+ "identified": true,
299
+ "now": 1,
300
+ "expected": { "ruleId": "r2", "effect": "deny", "satisfied": true }
301
+ },
302
+ {
303
+ "name": "anonymous rule with challenge effect collapses to deny (no credential leg can ever satisfy it)",
304
+ "rules": [{ "id": "r1", "match": {}, "effect": "challenge" }],
305
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
306
+ "grants": [],
307
+ "identified": false,
308
+ "now": 1,
309
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": true }
310
+ },
311
+ {
312
+ "name": "anonymous allow rule passes regardless of any carried grants",
313
+ "rules": [{ "id": "r1", "match": {}, "effect": "allow" }],
314
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
315
+ "grants": ["email:carol@acme.com"],
316
+ "identified": true,
317
+ "now": 1,
318
+ "expected": { "ruleId": "r1", "effect": "allow", "satisfied": true }
319
+ },
320
+ {
321
+ "name": "no rules at all: public allow",
322
+ "rules": [],
323
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
324
+ "grants": [],
325
+ "identified": false,
326
+ "now": 1,
327
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
328
+ },
329
+ {
330
+ "name": "no rule matches this host/path: public allow",
331
+ "rules": [{ "id": "r1", "match": { "pathPattern": "/private/**" }, "effect": "deny" }],
332
+ "context": { "host": "acme.example.com", "path": "/public/x", "ip": "", "country": "" },
333
+ "grants": [],
334
+ "identified": false,
335
+ "now": 1,
336
+ "expected": { "ruleId": null, "effect": "allow", "satisfied": true }
337
+ },
338
+ {
339
+ "name": "an irrelevant carried grant (same as a revoked/tombstoned id absent from requiredGrants) is a normal non-intersecting case",
340
+ "rules": [
341
+ {
342
+ "id": "r1",
343
+ "match": {},
344
+ "effect": "challenge",
345
+ "auth": { "requiredGrants": ["team:t1:member"] }
346
+ }
347
+ ],
348
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
349
+ "grants": ["email:zoe@acme.com"],
350
+ "identified": true,
351
+ "now": 1,
352
+ "expected": { "ruleId": "r1", "effect": "deny", "satisfied": false }
353
+ },
354
+ {
355
+ "name": "a later invited reviewer falls through earlier identity allows and passes their own",
356
+ "rules": [
357
+ {
358
+ "id": "cast-reviewer-a",
359
+ "match": {},
360
+ "effect": "allow",
361
+ "managedBy": "cast_reviewer",
362
+ "auth": { "requiredGrants": ["email:reviewer.a@example.com"] }
363
+ },
364
+ {
365
+ "id": "cast-reviewer-b",
366
+ "match": {},
367
+ "effect": "allow",
368
+ "managedBy": "cast_reviewer",
369
+ "auth": { "requiredGrants": ["email:reviewer.b@example.com"] }
370
+ },
371
+ {
372
+ "id": "cast-reviewers-audience",
373
+ "match": {},
374
+ "effect": "challenge",
375
+ "managedBy": "cast_reviewer",
376
+ "auth": {
377
+ "requiredGrants": ["email:reviewer.a@example.com", "email:reviewer.b@example.com"]
378
+ }
379
+ }
380
+ ],
381
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
382
+ "grants": ["email:reviewer.b@example.com"],
383
+ "identified": true,
384
+ "now": 1,
385
+ "expected": { "ruleId": "cast-reviewer-b", "effect": "allow", "satisfied": true }
386
+ },
387
+ {
388
+ "name": "a non-reviewer falls through identity allows and is denied by the terminal audience",
389
+ "rules": [
390
+ {
391
+ "id": "cast-reviewer-a",
392
+ "match": {},
393
+ "effect": "allow",
394
+ "managedBy": "cast_reviewer",
395
+ "auth": { "requiredGrants": ["email:reviewer.a@example.com"] }
396
+ },
397
+ {
398
+ "id": "cast-reviewer-b",
399
+ "match": {},
400
+ "effect": "allow",
401
+ "managedBy": "cast_reviewer",
402
+ "auth": { "requiredGrants": ["email:reviewer.b@example.com"] }
403
+ },
404
+ {
405
+ "id": "cast-reviewers-audience",
406
+ "match": {},
407
+ "effect": "challenge",
408
+ "managedBy": "cast_reviewer",
409
+ "auth": {
410
+ "requiredGrants": ["email:reviewer.a@example.com", "email:reviewer.b@example.com"]
411
+ }
412
+ }
413
+ ],
414
+ "context": { "host": "acme.example.com", "path": "/", "ip": "", "country": "" },
415
+ "grants": ["email:outsider@example.com"],
416
+ "identified": true,
417
+ "now": 1,
418
+ "expected": { "ruleId": "cast-reviewers-audience", "effect": "deny", "satisfied": false }
419
+ }
420
+ ]
421
+ }
@@ -16,7 +16,7 @@ export type PolicyAuthorizeInput = {
16
16
  resource: string | readonly string[];
17
17
  conditions?: RequestCondition;
18
18
  };
19
- export declare const publishActions: ("spaces:read" | "spaces:create" | "spaces:write" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:create")[];
19
+ export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:create")[];
20
20
  export declare function compileApiKeyPreset(preset: ApiKeyPreset, input: PolicyCompileInput): ApiCredentialPolicy;
21
21
  /** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
22
22
  export declare function apiKeyPresetPermissions(preset: ApiKeyPreset): StatticAction[];
@@ -0,0 +1,29 @@
1
+ import { type DnsInstruction } from "../contracts/common.js";
2
+ export type AddressDnsInstruction = DnsInstruction & {
3
+ type: "A" | "AAAA";
4
+ };
5
+ export type UnsupportedAddressDnsRecord = {
6
+ type: "A" | "AAAA";
7
+ name: string;
8
+ value: string;
9
+ };
10
+ export declare function addressDnsInstructionsForHostname(input: {
11
+ hostname: string;
12
+ dnsInstructions: unknown[];
13
+ }): AddressDnsInstruction[];
14
+ export declare function isWpCloudInboundIpv4(value: string): boolean;
15
+ export declare function addressDnsInstructionIsObserved(input: {
16
+ instruction: Pick<AddressDnsInstruction, "type" | "value">;
17
+ records: {
18
+ a: string[];
19
+ aaaa: string[];
20
+ };
21
+ }): boolean;
22
+ export declare function unsupportedAddressDnsRecordsForHostname(input: {
23
+ hostname: string;
24
+ dnsInstructions: unknown[];
25
+ records: {
26
+ a: string[];
27
+ aaaa: string[];
28
+ };
29
+ }): UnsupportedAddressDnsRecord[];
@@ -0,0 +1,78 @@
1
+ import { DnsInstructionSchema } from "../contracts/common.js";
2
+ function normalizeDnsName(value) {
3
+ return value.trim().toLowerCase().replace(/\.$/, "");
4
+ }
5
+ export function addressDnsInstructionsForHostname(input) {
6
+ const hostname = normalizeDnsName(input.hostname);
7
+ return input.dnsInstructions.flatMap((candidate) => {
8
+ const parsed = DnsInstructionSchema.safeParse(candidate);
9
+ if (!parsed.success) {
10
+ return [];
11
+ }
12
+ const instruction = parsed.data;
13
+ if ((instruction.type !== "A" && instruction.type !== "AAAA") ||
14
+ instruction.purpose !== "routing" ||
15
+ normalizeDnsName(instruction.name) !== hostname) {
16
+ return [];
17
+ }
18
+ return [
19
+ {
20
+ type: instruction.type,
21
+ name: instruction.name,
22
+ value: instruction.value,
23
+ purpose: instruction.purpose,
24
+ ...(instruction.ttl === undefined ? {} : { ttl: instruction.ttl }),
25
+ },
26
+ ];
27
+ });
28
+ }
29
+ function normalizeAddress(value) {
30
+ const address = value.trim().toLowerCase();
31
+ if (!address.includes(":")) {
32
+ return address;
33
+ }
34
+ try {
35
+ return new URL(`http://[${address}]`).hostname.slice(1, -1);
36
+ }
37
+ catch {
38
+ return address;
39
+ }
40
+ }
41
+ export function isWpCloudInboundIpv4(value) {
42
+ const match = /^192\.0\.79\.(\d{1,3})$/.exec(value);
43
+ if (!match) {
44
+ return false;
45
+ }
46
+ const octet = Number(match[1]);
47
+ return octet >= 128 && octet <= 191;
48
+ }
49
+ function instructionAcceptsObservedAddress(instruction, observedValue) {
50
+ const expected = normalizeAddress(instruction.value);
51
+ const observed = normalizeAddress(observedValue);
52
+ return observed === expected || (instruction.type === "A" && isWpCloudInboundIpv4(observed));
53
+ }
54
+ export function addressDnsInstructionIsObserved(input) {
55
+ const observed = input.instruction.type === "A" ? input.records.a : input.records.aaaa;
56
+ return observed.some((value) => instructionAcceptsObservedAddress(input.instruction, value));
57
+ }
58
+ export function unsupportedAddressDnsRecordsForHostname(input) {
59
+ const instructions = addressDnsInstructionsForHostname(input);
60
+ if (instructions.length === 0) {
61
+ return [];
62
+ }
63
+ const records = [
64
+ ...input.records.a.map((value) => ({ type: "A", value })),
65
+ ...input.records.aaaa.map((value) => ({ type: "AAAA", value })),
66
+ ].filter((record) => !instructions.some((instruction) => instruction.type === record.type &&
67
+ instructionAcceptsObservedAddress(instruction, record.value)));
68
+ const unique = new Set();
69
+ return records.flatMap((record) => {
70
+ const value = record.value.trim();
71
+ const key = `${record.type}:${value}`;
72
+ if (!value || unique.has(key)) {
73
+ return [];
74
+ }
75
+ unique.add(key);
76
+ return [{ ...record, name: input.hostname }];
77
+ });
78
+ }
@@ -0,0 +1,29 @@
1
+ export type HostnameDiagnosticsForActions = {
2
+ hostname: string;
3
+ providerIps: string[];
4
+ dnsInstructions: unknown[];
5
+ dns: {
6
+ pointsToProvider: boolean;
7
+ txtVerified: boolean | null;
8
+ expectedTxtValue: string | null;
9
+ records?: {
10
+ a: string[];
11
+ aaaa: string[];
12
+ };
13
+ };
14
+ ssl: {
15
+ status: string;
16
+ canRetry: boolean;
17
+ brokenRecord: {
18
+ reason: string | null;
19
+ lastError: string | null;
20
+ } | null;
21
+ };
22
+ };
23
+ export type DomainRequiredAction = {
24
+ code: string;
25
+ hostname: string;
26
+ message: string;
27
+ };
28
+ /** Turn hostname diagnostics into the actions a user must take to finish binding. */
29
+ export declare function domainRequiredActions(hostnameDiagnostics: HostnameDiagnosticsForActions[]): DomainRequiredAction[];
@@ -0,0 +1,63 @@
1
+ import { addressDnsInstructionIsObserved, addressDnsInstructionsForHostname, unsupportedAddressDnsRecordsForHostname, } from "./dns-instructions.js";
2
+ /** Turn hostname diagnostics into the actions a user must take to finish binding. */
3
+ export function domainRequiredActions(hostnameDiagnostics) {
4
+ const actions = [];
5
+ for (const entry of hostnameDiagnostics) {
6
+ const unsupportedRecords = entry.dns.records
7
+ ? unsupportedAddressDnsRecordsForHostname({ ...entry, records: entry.dns.records })
8
+ : [];
9
+ actions.push(...unsupportedRecords.map((record) => ({
10
+ code: "dns_unaccepted_address",
11
+ hostname: entry.hostname,
12
+ message: `Remove the ${record.type} record on ${record.name} with value ${record.value}; it does not match a Spacefast target for this hostname.`,
13
+ })));
14
+ if (!entry.dns.pointsToProvider) {
15
+ const routingInstructions = addressDnsInstructionsForHostname(entry);
16
+ const observedAddressRecords = entry.dns.records;
17
+ const missingRoutingInstructions = observedAddressRecords
18
+ ? routingInstructions.filter((instruction) => !addressDnsInstructionIsObserved({
19
+ instruction,
20
+ records: observedAddressRecords,
21
+ }))
22
+ : routingInstructions;
23
+ if (missingRoutingInstructions.length > 0) {
24
+ actions.push(...missingRoutingInstructions.map((instruction) => ({
25
+ code: "dns_not_pointing",
26
+ hostname: entry.hostname,
27
+ message: `Add an ${instruction.type} record on ${instruction.name} with value ${instruction.value}.`,
28
+ })));
29
+ }
30
+ else if (routingInstructions.length === 0) {
31
+ const targets = entry.providerIps.length > 0 ? entry.providerIps.join(", ") : "Spacefast";
32
+ actions.push({
33
+ code: "dns_not_pointing",
34
+ hostname: entry.hostname,
35
+ message: `Point ${entry.hostname} at ${targets} using the DNS record type shown by Spacefast.`,
36
+ });
37
+ }
38
+ }
39
+ if (entry.dns.txtVerified === false && entry.dns.expectedTxtValue) {
40
+ actions.push({
41
+ code: "txt_verification_missing",
42
+ hostname: entry.hostname,
43
+ message: `Add a TXT record on ${entry.hostname} with value ${entry.dns.expectedTxtValue}.`,
44
+ });
45
+ }
46
+ if (entry.ssl.brokenRecord) {
47
+ const reason = entry.ssl.brokenRecord.reason ?? entry.ssl.brokenRecord.lastError;
48
+ actions.push({
49
+ code: "ssl_blocking_record",
50
+ hostname: entry.hostname,
51
+ message: `Fix the DNS record blocking SSL on ${entry.hostname}${reason ? ` (${reason})` : ""}.`,
52
+ });
53
+ }
54
+ else if (entry.ssl.status !== "active" && entry.ssl.canRetry) {
55
+ actions.push({
56
+ code: "ssl_retry_available",
57
+ hostname: entry.hostname,
58
+ message: `SSL is ${entry.ssl.status} on ${entry.hostname}; retry with \`sf domains check ${entry.hostname}\`.`,
59
+ });
60
+ }
61
+ }
62
+ return actions;
63
+ }