@fungi.computer/puffball 0.0.0-stage → 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +443 -2
- package/dist/app-api.d.ts +7 -0
- package/dist/app-api.js +61 -0
- package/dist/app-backend-binding-vocabulary.d.ts +299 -0
- package/dist/app-backend-binding-vocabulary.js +257 -0
- package/dist/app-backend-bindings.d.ts +21 -0
- package/dist/app-backend-bindings.js +47 -0
- package/dist/app-declaration.d.ts +46 -0
- package/dist/app-declaration.js +115 -0
- package/dist/app-event-facts.d.ts +40 -0
- package/dist/app-event-facts.js +28 -0
- package/dist/app-method-context.d.ts +109 -0
- package/dist/app-method-context.js +9 -0
- package/dist/app-method-runtime.d.ts +86 -0
- package/dist/app-method-runtime.js +122 -0
- package/dist/app-origin-paths.d.ts +6 -0
- package/dist/app-origin-paths.js +6 -0
- package/dist/app-vocabulary.d.ts +189 -0
- package/dist/app-vocabulary.js +103 -0
- package/dist/app.d.ts +379 -0
- package/dist/app.js +71 -0
- package/dist/artifact-dispositions.d.ts +115 -0
- package/dist/artifact-dispositions.js +234 -0
- package/dist/assets.d.ts +54 -0
- package/dist/assets.js +89 -0
- package/dist/backend.d.ts +41 -0
- package/dist/backend.js +29 -0
- package/dist/background-replay.d.ts +115 -0
- package/dist/background-replay.js +175 -0
- package/dist/buyer-references.d.ts +69 -0
- package/dist/buyer-references.js +154 -0
- package/dist/cloudflare-assets.d.ts +48 -0
- package/dist/cloudflare-assets.js +105 -0
- package/dist/cloudflare.d.ts +254 -0
- package/dist/cloudflare.js +1619 -0
- package/dist/effect.d.ts +73 -0
- package/dist/effect.js +325 -0
- package/dist/git-representation.d.ts +13 -0
- package/dist/git-representation.js +16 -0
- package/dist/inbox.d.ts +132 -0
- package/dist/inbox.js +81 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +85 -0
- package/dist/internal/app-description.d.ts +14 -0
- package/dist/internal/app-description.js +98 -0
- package/dist/internal/app-release-artifact-reader.d.ts +26 -0
- package/dist/internal/app-release-artifact-reader.js +1 -0
- package/dist/internal/app-release-contract.d.ts +540 -0
- package/dist/internal/app-release-contract.js +169 -0
- package/dist/internal/app-release-schema.d.ts +1676 -0
- package/dist/internal/app-release-schema.js +386 -0
- package/dist/internal/app-release-store.d.ts +47 -0
- package/dist/internal/app-release-store.js +1 -0
- package/dist/internal/background-sync.d.ts +7 -0
- package/dist/internal/background-sync.js +10 -0
- package/dist/internal/deterministic-memory-app-release-store.d.ts +14 -0
- package/dist/internal/deterministic-memory-app-release-store.js +319 -0
- package/dist/internal/failure.d.ts +2 -0
- package/dist/internal/failure.js +9 -0
- package/dist/internal/immutable-release-material.d.ts +3 -0
- package/dist/internal/immutable-release-material.js +13 -0
- package/dist/internal/promise-app-release-store-adapter.d.ts +9 -0
- package/dist/internal/promise-app-release-store-adapter.js +65 -0
- package/dist/internal/promise-artifact-reader-adapter.d.ts +9 -0
- package/dist/internal/promise-artifact-reader-adapter.js +18 -0
- package/dist/internal/public-route-sql.d.ts +10 -0
- package/dist/internal/public-route-sql.js +239 -0
- package/dist/internal/public-website-authority.d.ts +338 -0
- package/dist/internal/public-website-authority.js +50 -0
- package/dist/internal/public-website-delivery.d.ts +59 -0
- package/dist/internal/public-website-delivery.js +232 -0
- package/dist/internal/public-website-fetch.d.ts +8 -0
- package/dist/internal/public-website-fetch.js +54 -0
- package/dist/internal/release-transition-material.d.ts +3 -0
- package/dist/internal/release-transition-material.js +11 -0
- package/dist/internal/verified-artifact-snapshot.d.ts +41 -0
- package/dist/internal/verified-artifact-snapshot.js +91 -0
- package/dist/public-route-moderation.d.ts +83 -0
- package/dist/public-route-moderation.js +236 -0
- package/dist/public-route-policy.d.ts +51 -0
- package/dist/public-route-policy.js +88 -0
- package/dist/public-routes.d.ts +34 -0
- package/dist/public-routes.js +136 -0
- package/dist/publisher.d.ts +61 -0
- package/dist/publisher.js +16 -0
- package/dist/safety-takedown.d.ts +56 -0
- package/dist/safety-takedown.js +146 -0
- package/dist/seller-closure-notice.d.ts +14 -0
- package/dist/seller-closure-notice.js +14 -0
- package/dist/seller-closure.d.ts +15 -0
- package/dist/seller-closure.js +31 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +47 -0
- package/docs/api/README.md +26 -0
- package/docs/api/app-backend-binding-vocabulary.md +276 -0
- package/docs/api/app-backend-bindings.md +141 -0
- package/docs/api/app-event-facts.md +110 -0
- package/docs/api/app-method-context.md +166 -0
- package/docs/api/app-method-runtime.md +330 -0
- package/docs/api/backend.md +108 -0
- package/docs/api/background-replay.md +516 -0
- package/docs/api/cloudflare.md +1044 -0
- package/docs/api/effect.md +2429 -0
- package/docs/api/git-representation.md +56 -0
- package/docs/api/inbox.md +289 -0
- package/docs/api/index.md +2868 -0
- package/docs/api/publisher.md +147 -0
- package/docs/api/testing.md +31 -0
- package/package.json +158 -3
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { appIdSchema, publicSurfaceIdSchema, stableRouteSchema, } from "./internal/app-release-schema.js";
|
|
3
|
+
import { publicRouteKey, publicRoutePolicy } from "./public-route-policy.js";
|
|
4
|
+
import { reportFailure } from "./internal/failure.js";
|
|
5
|
+
const identity = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
|
|
6
|
+
const commandSchema = Schema.Struct({
|
|
7
|
+
operationId: identity,
|
|
8
|
+
operatorId: identity,
|
|
9
|
+
reason: Schema.String.pipe(Schema.check(Schema.isPattern(/\S/u)), Schema.check(Schema.isMaxLength(1000))),
|
|
10
|
+
target: Schema.Struct({
|
|
11
|
+
publicSurfaceId: publicSurfaceIdSchema,
|
|
12
|
+
appId: appIdSchema,
|
|
13
|
+
teamId: identity,
|
|
14
|
+
stableRoute: stableRouteSchema,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
const operationSchema = Schema.Struct({
|
|
18
|
+
operation_id: identity,
|
|
19
|
+
kind: Schema.Literals(["unlist-and-release", "re-allow"]),
|
|
20
|
+
public_surface_id: publicSurfaceIdSchema,
|
|
21
|
+
app_id: appIdSchema,
|
|
22
|
+
team_id: identity,
|
|
23
|
+
stable_route: stableRouteSchema,
|
|
24
|
+
route_key: Schema.String,
|
|
25
|
+
reason: Schema.String,
|
|
26
|
+
initiated_by: identity,
|
|
27
|
+
created_at: Schema.Number,
|
|
28
|
+
removal_operation_id: Schema.NullOr(identity),
|
|
29
|
+
state: Schema.Literals(["pending", "complete", "superseded-by-erasure"]),
|
|
30
|
+
completed_at: Schema.NullOr(Schema.Number),
|
|
31
|
+
});
|
|
32
|
+
/** Publisher phases only. Its Operator composes the Marketplace phase between prepare and complete. */
|
|
33
|
+
export function createD1PublicRouteModeration(database, now = Date.now) {
|
|
34
|
+
const read = async (operationId) => {
|
|
35
|
+
const row = await database
|
|
36
|
+
.prepare("SELECT * FROM app_release_route_operations WHERE operation_id=?")
|
|
37
|
+
.bind(operationId)
|
|
38
|
+
.first();
|
|
39
|
+
return row === null ? null : Schema.decodeUnknownSync(operationSchema)(row);
|
|
40
|
+
};
|
|
41
|
+
const protect = async (action) => {
|
|
42
|
+
try {
|
|
43
|
+
return await action();
|
|
44
|
+
}
|
|
45
|
+
catch (cause) {
|
|
46
|
+
reportFailure("public route moderation", cause);
|
|
47
|
+
return { kind: "unavailable" };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const matches = (operation, input, kind, removalId) => operation.kind === kind &&
|
|
51
|
+
operation.public_surface_id === input.target.publicSurfaceId &&
|
|
52
|
+
operation.app_id === input.target.appId &&
|
|
53
|
+
operation.team_id === input.target.teamId &&
|
|
54
|
+
operation.stable_route === input.target.stableRoute &&
|
|
55
|
+
operation.reason === input.reason &&
|
|
56
|
+
operation.removal_operation_id === removalId;
|
|
57
|
+
const prepare = (raw, removalId) => protect(async () => {
|
|
58
|
+
const input = Schema.decodeUnknownSync(commandSchema, {
|
|
59
|
+
onExcessProperty: "error",
|
|
60
|
+
})(raw);
|
|
61
|
+
const kind = removalId === null ? "unlist-and-release" : "re-allow";
|
|
62
|
+
const existing = await read(input.operationId);
|
|
63
|
+
if (existing)
|
|
64
|
+
return matches(existing, input, kind, removalId) &&
|
|
65
|
+
existing.state !== "superseded-by-erasure"
|
|
66
|
+
? { kind: "recorded", operation: existing }
|
|
67
|
+
: { kind: "conflict" };
|
|
68
|
+
const at = Math.floor(now() / 1000);
|
|
69
|
+
const key = publicRouteKey(input.target.stableRoute);
|
|
70
|
+
const material = [
|
|
71
|
+
input.operationId,
|
|
72
|
+
kind,
|
|
73
|
+
input.target.publicSurfaceId,
|
|
74
|
+
input.target.appId,
|
|
75
|
+
input.target.teamId,
|
|
76
|
+
input.target.stableRoute,
|
|
77
|
+
key,
|
|
78
|
+
input.reason,
|
|
79
|
+
input.operatorId,
|
|
80
|
+
at,
|
|
81
|
+
removalId,
|
|
82
|
+
];
|
|
83
|
+
const eligible = removalId === null
|
|
84
|
+
? `EXISTS (SELECT 1 FROM app_release_route_claims c
|
|
85
|
+
JOIN app_release_public_surfaces s ON s.public_surface_id=c.public_surface_id
|
|
86
|
+
WHERE c.public_surface_id=? AND s.app_id=? AND c.team_id=? AND c.stable_route=? AND c.state='held')`
|
|
87
|
+
: `EXISTS (SELECT 1 FROM app_release_team_route_blocks b
|
|
88
|
+
JOIN app_release_route_operations removal ON removal.operation_id=b.removal_operation_id
|
|
89
|
+
WHERE removal.public_surface_id=? AND removal.app_id=? AND b.team_id=? AND removal.stable_route=?
|
|
90
|
+
AND b.removal_operation_id=? AND removal.state='complete')`;
|
|
91
|
+
const guardValues = [
|
|
92
|
+
input.target.publicSurfaceId,
|
|
93
|
+
input.target.appId,
|
|
94
|
+
input.target.teamId,
|
|
95
|
+
input.target.stableRoute,
|
|
96
|
+
...(removalId === null ? [] : [removalId]),
|
|
97
|
+
];
|
|
98
|
+
// Every mutation joins the full bound material, so a racing changed-ID replay cannot advance it.
|
|
99
|
+
const guard = `SELECT operation_id FROM app_release_route_operations
|
|
100
|
+
WHERE operation_id=? AND kind=? AND public_surface_id=? AND app_id=? AND team_id=?
|
|
101
|
+
AND stable_route=? AND route_key=? AND reason=? AND state='pending'
|
|
102
|
+
AND removal_operation_id IS ?`;
|
|
103
|
+
const guardBindings = [...material.slice(0, 8), removalId];
|
|
104
|
+
const statements = [
|
|
105
|
+
database
|
|
106
|
+
.prepare(`INSERT INTO app_release_route_operations
|
|
107
|
+
(operation_id,kind,public_surface_id,app_id,team_id,stable_route,route_key,reason,initiated_by,created_at,removal_operation_id,state)
|
|
108
|
+
SELECT ?,?,?,?,?,?,?,?,?,?,?,'pending' WHERE ${eligible} ON CONFLICT DO NOTHING`)
|
|
109
|
+
.bind(...material, ...guardValues),
|
|
110
|
+
];
|
|
111
|
+
if (removalId === null)
|
|
112
|
+
statements.push(database
|
|
113
|
+
.prepare(`UPDATE app_release_route_claims SET state='removing',removal_operation_id=?
|
|
114
|
+
WHERE public_surface_id=? AND state='held' AND EXISTS (${guard})`)
|
|
115
|
+
.bind(input.operationId, input.target.publicSurfaceId, ...guardBindings), database
|
|
116
|
+
.prepare(`INSERT INTO app_release_surface_retirements(public_surface_id,operation_id)
|
|
117
|
+
SELECT public_surface_id,operation_id FROM app_release_route_operations WHERE operation_id IN (${guard})
|
|
118
|
+
ON CONFLICT DO NOTHING`)
|
|
119
|
+
.bind(...guardBindings), database
|
|
120
|
+
.prepare(`INSERT INTO app_release_team_route_blocks(team_id,route_key,stable_route,removal_operation_id)
|
|
121
|
+
SELECT team_id,route_key,stable_route,operation_id FROM app_release_route_operations WHERE operation_id IN (${guard})
|
|
122
|
+
ON CONFLICT(team_id,route_key) DO UPDATE SET removal_operation_id=excluded.removal_operation_id,stable_route=excluded.stable_route`)
|
|
123
|
+
.bind(...guardBindings));
|
|
124
|
+
statements.push(database
|
|
125
|
+
.prepare(`INSERT INTO app_release_route_operation_steps(operation_id,phase,operator_id,recorded_at)
|
|
126
|
+
SELECT operation_id,'prepared',?,? FROM app_release_route_operations WHERE operation_id IN (${guard}) ON CONFLICT DO NOTHING`)
|
|
127
|
+
.bind(input.operatorId, at, ...guardBindings));
|
|
128
|
+
const results = await database.batch(statements);
|
|
129
|
+
if (results.length !== statements.length ||
|
|
130
|
+
results.some((result) => !result.success))
|
|
131
|
+
throw new Error("Route moderation batch unavailable");
|
|
132
|
+
const operation = await read(input.operationId);
|
|
133
|
+
return operation && matches(operation, input, kind, removalId)
|
|
134
|
+
? { kind: "recorded", operation }
|
|
135
|
+
: { kind: "conflict" };
|
|
136
|
+
});
|
|
137
|
+
/** Call only after the corresponding Marketplace owner returns its committed phase. */
|
|
138
|
+
const complete = (operationId, operatorId) => protect(async () => {
|
|
139
|
+
const id = Schema.decodeUnknownSync(identity)(operationId);
|
|
140
|
+
const actor = Schema.decodeUnknownSync(identity)(operatorId);
|
|
141
|
+
const operation = await read(id);
|
|
142
|
+
if (!operation || operation.state === "superseded-by-erasure")
|
|
143
|
+
return { kind: "conflict" };
|
|
144
|
+
if (operation.state === "complete")
|
|
145
|
+
return { kind: "recorded", operation };
|
|
146
|
+
const at = Math.floor(now() / 1000);
|
|
147
|
+
const statements = [];
|
|
148
|
+
if (operation.kind === "unlist-and-release") {
|
|
149
|
+
// The exact still-removing claim is the only source of a new hold. A stale replay cannot freeze a later claimant.
|
|
150
|
+
statements.push(database
|
|
151
|
+
.prepare(`INSERT INTO app_release_route_holds(route_key,stable_route,until_s,cause,operation_id)
|
|
152
|
+
SELECT c.route_key,c.stable_route,?,'operator-release',o.operation_id
|
|
153
|
+
FROM app_release_route_claims c JOIN app_release_route_operations o
|
|
154
|
+
ON o.operation_id=c.removal_operation_id AND o.public_surface_id=c.public_surface_id
|
|
155
|
+
WHERE o.operation_id=? AND o.state='pending' AND c.state='removing'
|
|
156
|
+
ON CONFLICT(route_key) DO UPDATE SET until_s=max(until_s,excluded.until_s),
|
|
157
|
+
cause=CASE WHEN cause='erasure' THEN cause ELSE excluded.cause END,
|
|
158
|
+
operation_id=CASE WHEN cause='erasure' THEN operation_id ELSE excluded.operation_id END`)
|
|
159
|
+
.bind(at + publicRoutePolicy.releaseCooldownSeconds, id));
|
|
160
|
+
statements.push(database
|
|
161
|
+
.prepare(`DELETE FROM app_release_route_claims
|
|
162
|
+
WHERE public_surface_id=? AND state='removing' AND removal_operation_id=?
|
|
163
|
+
AND EXISTS (SELECT 1 FROM app_release_route_holds h WHERE h.route_key=app_release_route_claims.route_key AND h.until_s>=?)`)
|
|
164
|
+
.bind(operation.public_surface_id, id, at + publicRoutePolicy.releaseCooldownSeconds));
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
statements.push(database
|
|
168
|
+
.prepare(`DELETE FROM app_release_team_route_blocks
|
|
169
|
+
WHERE team_id=? AND route_key=? AND removal_operation_id=?`)
|
|
170
|
+
.bind(operation.team_id, operation.route_key, operation.removal_operation_id));
|
|
171
|
+
const completed = operation.kind === "unlist-and-release"
|
|
172
|
+
? `NOT EXISTS (SELECT 1 FROM app_release_route_claims c WHERE c.public_surface_id=app_release_route_operations.public_surface_id)
|
|
173
|
+
AND EXISTS (SELECT 1 FROM app_release_route_holds h WHERE h.route_key=app_release_route_operations.route_key AND h.operation_id=app_release_route_operations.operation_id)`
|
|
174
|
+
: `NOT EXISTS (SELECT 1 FROM app_release_team_route_blocks b WHERE b.team_id=app_release_route_operations.team_id AND b.route_key=app_release_route_operations.route_key)`;
|
|
175
|
+
statements.push(database
|
|
176
|
+
.prepare(`UPDATE app_release_route_operations SET state='complete',completed_at=?
|
|
177
|
+
WHERE operation_id=? AND state='pending' AND ${completed}`)
|
|
178
|
+
.bind(at, id));
|
|
179
|
+
statements.push(database
|
|
180
|
+
.prepare(`INSERT INTO app_release_route_operation_steps(operation_id,phase,operator_id,recorded_at)
|
|
181
|
+
SELECT operation_id,'completed',?,? FROM app_release_route_operations WHERE operation_id=? AND state='complete'
|
|
182
|
+
ON CONFLICT DO NOTHING`)
|
|
183
|
+
.bind(actor, at, id));
|
|
184
|
+
const results = await database.batch(statements);
|
|
185
|
+
if (results.length !== statements.length ||
|
|
186
|
+
results.some((result) => !result.success))
|
|
187
|
+
throw new Error("Route moderation batch unavailable");
|
|
188
|
+
const result = await read(id);
|
|
189
|
+
return result?.state === "complete"
|
|
190
|
+
? { kind: "recorded", operation: result }
|
|
191
|
+
: { kind: "conflict" };
|
|
192
|
+
});
|
|
193
|
+
const pending = async (after = "") => {
|
|
194
|
+
const cursor = Schema.decodeUnknownSync(Schema.String.pipe(Schema.check(Schema.isMaxLength(256))))(after);
|
|
195
|
+
const result = await database
|
|
196
|
+
.prepare("SELECT * FROM app_release_route_operations WHERE state='pending' AND operation_id>? ORDER BY operation_id LIMIT 100")
|
|
197
|
+
.bind(cursor)
|
|
198
|
+
.all();
|
|
199
|
+
return result.results.map((row) => Schema.decodeUnknownSync(operationSchema)(row));
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
prepareRemoval: (input) => prepare(input, null),
|
|
203
|
+
prepareReallow: (input, removalId) => prepare(input, Schema.decodeUnknownSync(identity)(removalId)),
|
|
204
|
+
complete,
|
|
205
|
+
pending,
|
|
206
|
+
read,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const platformReplaySchema = Schema.Struct({
|
|
210
|
+
publicSurfaceId: publicSurfaceIdSchema,
|
|
211
|
+
appId: appIdSchema,
|
|
212
|
+
stableRoute: stableRouteSchema,
|
|
213
|
+
teamId: identity,
|
|
214
|
+
operatorId: identity,
|
|
215
|
+
});
|
|
216
|
+
/** Private Operator door only: Hub supplies the resolved System Team and admitted actor. */
|
|
217
|
+
export async function recordD1PlatformOwnerReplay(database, raw) {
|
|
218
|
+
const input = Schema.decodeUnknownSync(platformReplaySchema)(raw);
|
|
219
|
+
const row = await database
|
|
220
|
+
.prepare(`
|
|
221
|
+
INSERT INTO app_release_platform_owner_replays
|
|
222
|
+
(public_surface_id,app_id,stable_route,owner_team_id,publisher_user_id,operator_user_id,recorded_at)
|
|
223
|
+
SELECT s.public_surface_id,s.app_id,s.stable_route,s.publisher_team_id,s.publisher_user_id,?,unixepoch()
|
|
224
|
+
FROM app_release_public_surfaces s
|
|
225
|
+
JOIN app_release_route_claims c ON c.public_surface_id=s.public_surface_id AND c.route_key=s.route_key
|
|
226
|
+
JOIN app_release_platform_routes p ON p.app_id=s.app_id AND p.stable_route=s.stable_route AND p.route_key=s.route_key
|
|
227
|
+
WHERE s.public_surface_id=? AND s.app_id=? AND s.stable_route=?
|
|
228
|
+
AND s.publisher_team_id=? AND c.team_id=s.publisher_team_id
|
|
229
|
+
AND c.user_id=s.publisher_user_id AND c.state='held' AND s.authority_kind='platform'
|
|
230
|
+
AND s.publisher_user_id<>?
|
|
231
|
+
AND NOT EXISTS (SELECT 1 FROM app_release_surface_retirements x WHERE x.public_surface_id=s.public_surface_id)
|
|
232
|
+
RETURNING public_surface_id`)
|
|
233
|
+
.bind(input.operatorId, input.publicSurfaceId, input.appId, input.stableRoute, input.teamId, input.operatorId)
|
|
234
|
+
.first();
|
|
235
|
+
return row !== null;
|
|
236
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** CTO customer-publication policy. Deployment projects this one configuration. */
|
|
3
|
+
export declare const publicRoutePolicy: {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly maxPublicRoutesPerTeam: 20;
|
|
6
|
+
readonly maxUnusedSurfacesPerApp: 20;
|
|
7
|
+
readonly releaseCooldownSeconds: number;
|
|
8
|
+
readonly erasureTombstoneSeconds: number;
|
|
9
|
+
readonly reservedWords: readonly ["admin", "administrator", "api", "assets", "auth", "billing", "botanical", "docs", "fungi", "help", "login", "logout", "oauth", "operator", "security", "settings", "signin", "signup", "static", "status", "support", "www"];
|
|
10
|
+
readonly brandPrefixes: readonly ["fungi"];
|
|
11
|
+
readonly brandWordExceptions: readonly ["fungible", "fungicide"];
|
|
12
|
+
};
|
|
13
|
+
/** Comparison only: a key never repairs or replaces the canonical route URL. */
|
|
14
|
+
export declare function publicRouteKey(value: string): string;
|
|
15
|
+
/** Router prefixes and platform assignments are supplied by their owners. */
|
|
16
|
+
export declare function isReservedPublicRoute(route: string, structuralRoutes: readonly string[]): boolean;
|
|
17
|
+
/** Deployment supplies routes from the platform plan and App-origin router. */
|
|
18
|
+
export declare const publicRouteConfigurationSchema: Schema.Struct<{
|
|
19
|
+
readonly platformRoutes: Schema.$Array<Schema.Struct<{
|
|
20
|
+
readonly appId: Schema.refine<import("./index.js").AppId, Schema.String>;
|
|
21
|
+
readonly stableRoute: Schema.refine<import("./index.js").StableRoute, Schema.String>;
|
|
22
|
+
}>>;
|
|
23
|
+
readonly structuralRoutes: Schema.$Array<Schema.refine<import("./index.js").StableRoute, Schema.String>>;
|
|
24
|
+
}>;
|
|
25
|
+
export type PublicRouteConfiguration = Schema.Schema.Type<typeof publicRouteConfigurationSchema>;
|
|
26
|
+
/** The deployed checksum binds both maintained policy and environment assignments. */
|
|
27
|
+
export declare function capturePublicRoutePolicy(input: unknown): Promise<{
|
|
28
|
+
configuration: {
|
|
29
|
+
platformRoutes: {
|
|
30
|
+
readonly appId: import("./index.js").AppId;
|
|
31
|
+
readonly stableRoute: import("./index.js").StableRoute;
|
|
32
|
+
}[];
|
|
33
|
+
structuralRoutes: import("./index.js").StableRoute[];
|
|
34
|
+
};
|
|
35
|
+
json: string;
|
|
36
|
+
digest: string;
|
|
37
|
+
version: 1;
|
|
38
|
+
}>;
|
|
39
|
+
/** Decode the deployment JSON at its owned representation boundary. */
|
|
40
|
+
export declare function capturePublicRoutePolicyFromJson(input: string): Promise<{
|
|
41
|
+
configuration: {
|
|
42
|
+
platformRoutes: {
|
|
43
|
+
readonly appId: import("./index.js").AppId;
|
|
44
|
+
readonly stableRoute: import("./index.js").StableRoute;
|
|
45
|
+
}[];
|
|
46
|
+
structuralRoutes: import("./index.js").StableRoute[];
|
|
47
|
+
};
|
|
48
|
+
json: string;
|
|
49
|
+
digest: string;
|
|
50
|
+
version: 1;
|
|
51
|
+
}>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { appIdSchema, stableRouteSchema, } from "./internal/app-release-schema.js";
|
|
3
|
+
/** CTO customer-publication policy. Deployment projects this one configuration. */
|
|
4
|
+
export const publicRoutePolicy = {
|
|
5
|
+
version: 1,
|
|
6
|
+
maxPublicRoutesPerTeam: 20,
|
|
7
|
+
maxUnusedSurfacesPerApp: 20,
|
|
8
|
+
releaseCooldownSeconds: 7 * 24 * 60 * 60,
|
|
9
|
+
erasureTombstoneSeconds: 90 * 24 * 60 * 60,
|
|
10
|
+
reservedWords: [
|
|
11
|
+
"admin",
|
|
12
|
+
"administrator",
|
|
13
|
+
"api",
|
|
14
|
+
"assets",
|
|
15
|
+
"auth",
|
|
16
|
+
"billing",
|
|
17
|
+
"botanical",
|
|
18
|
+
"docs",
|
|
19
|
+
"fungi",
|
|
20
|
+
"help",
|
|
21
|
+
"login",
|
|
22
|
+
"logout",
|
|
23
|
+
"oauth",
|
|
24
|
+
"operator",
|
|
25
|
+
"security",
|
|
26
|
+
"settings",
|
|
27
|
+
"signin",
|
|
28
|
+
"signup",
|
|
29
|
+
"static",
|
|
30
|
+
"status",
|
|
31
|
+
"support",
|
|
32
|
+
"www",
|
|
33
|
+
],
|
|
34
|
+
brandPrefixes: ["fungi"],
|
|
35
|
+
brandWordExceptions: ["fungible", "fungicide"],
|
|
36
|
+
};
|
|
37
|
+
/** Comparison only: a key never repairs or replaces the canonical route URL. */
|
|
38
|
+
export function publicRouteKey(value) {
|
|
39
|
+
return (value.startsWith("/") ? value.slice(1) : value)
|
|
40
|
+
.replaceAll("-", "")
|
|
41
|
+
.replaceAll("_", "")
|
|
42
|
+
.replaceAll("1", "i")
|
|
43
|
+
.replaceAll("l", "i")
|
|
44
|
+
.replaceAll("0", "o")
|
|
45
|
+
.replaceAll("5", "s")
|
|
46
|
+
.replaceAll("3", "e");
|
|
47
|
+
}
|
|
48
|
+
/** Router prefixes and platform assignments are supplied by their owners. */
|
|
49
|
+
export function isReservedPublicRoute(route, structuralRoutes) {
|
|
50
|
+
const key = publicRouteKey(route);
|
|
51
|
+
if (structuralRoutes.some((value) => publicRouteKey(value) === key) ||
|
|
52
|
+
publicRoutePolicy.reservedWords.some((value) => publicRouteKey(value) === key))
|
|
53
|
+
return true;
|
|
54
|
+
return (publicRoutePolicy.brandPrefixes.some((value) => key.startsWith(publicRouteKey(value))) &&
|
|
55
|
+
!publicRoutePolicy.brandWordExceptions.some((value) => publicRouteKey(value) === key));
|
|
56
|
+
}
|
|
57
|
+
/** Deployment supplies routes from the platform plan and App-origin router. */
|
|
58
|
+
export const publicRouteConfigurationSchema = Schema.Struct({
|
|
59
|
+
platformRoutes: Schema.Array(Schema.Struct({
|
|
60
|
+
appId: appIdSchema,
|
|
61
|
+
stableRoute: stableRouteSchema,
|
|
62
|
+
})),
|
|
63
|
+
structuralRoutes: Schema.Array(stableRouteSchema),
|
|
64
|
+
}).pipe(Schema.check(Schema.makeFilter((configuration) => {
|
|
65
|
+
const keys = configuration.platformRoutes.map(({ stableRoute }) => publicRouteKey(stableRoute));
|
|
66
|
+
return new Set(keys).size === keys.length;
|
|
67
|
+
}, { message: "Platform route assignments conflict" })), Schema.check(Schema.makeFilter((configuration) => {
|
|
68
|
+
const structuralKeys = new Set(configuration.structuralRoutes.map(publicRouteKey));
|
|
69
|
+
return !configuration.platformRoutes.some(({ stableRoute }) => structuralKeys.has(publicRouteKey(stableRoute)));
|
|
70
|
+
}, { message: "Platform route assignments conflict with structural routes" })));
|
|
71
|
+
/** The deployed checksum binds both maintained policy and environment assignments. */
|
|
72
|
+
export async function capturePublicRoutePolicy(input) {
|
|
73
|
+
const decoded = Schema.decodeUnknownSync(publicRouteConfigurationSchema, {
|
|
74
|
+
onExcessProperty: "error",
|
|
75
|
+
})(input);
|
|
76
|
+
const configuration = {
|
|
77
|
+
platformRoutes: [...decoded.platformRoutes].sort((a, b) => a.stableRoute.localeCompare(b.stableRoute)),
|
|
78
|
+
structuralRoutes: [...decoded.structuralRoutes].sort(),
|
|
79
|
+
};
|
|
80
|
+
const json = JSON.stringify({ policy: publicRoutePolicy, configuration });
|
|
81
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(json)));
|
|
82
|
+
const digest = Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
83
|
+
return { configuration, json, digest, version: publicRoutePolicy.version };
|
|
84
|
+
}
|
|
85
|
+
/** Decode the deployment JSON at its owned representation boundary. */
|
|
86
|
+
export async function capturePublicRoutePolicyFromJson(input) {
|
|
87
|
+
return capturePublicRoutePolicy(Schema.decodeUnknownSync(Schema.fromJsonString(publicRouteConfigurationSchema), { onExcessProperty: "error" })(input));
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { AppReleaseD1Database } from "./cloudflare.js";
|
|
3
|
+
import { capturePublicRoutePolicy } from "./public-route-policy.js";
|
|
4
|
+
export type CapturedPublicRoutePolicy = Awaited<ReturnType<typeof capturePublicRoutePolicy>>;
|
|
5
|
+
/** Fail closed until the operator has installed this exact deployment policy. */
|
|
6
|
+
export declare function publicRoutePolicyReady(database: AppReleaseD1Database, policy: CapturedPublicRoutePolicy): Promise<boolean>;
|
|
7
|
+
/** Operator composition only; values come from its captured deployment, not a request. */
|
|
8
|
+
export declare function configureD1PublicRoutes(database: AppReleaseD1Database, policy: CapturedPublicRoutePolicy): Promise<void>;
|
|
9
|
+
/** Live seller custody; a buyer entitlement or a historical Surface is never ownership. */
|
|
10
|
+
export declare function ownsD1PublicRoute(database: AppReleaseD1Database, input: {
|
|
11
|
+
readonly teamId: string;
|
|
12
|
+
readonly appId: string;
|
|
13
|
+
readonly stableRoute: string;
|
|
14
|
+
}): Promise<boolean>;
|
|
15
|
+
declare const ownedPublicRouteSchema: Schema.Struct<{
|
|
16
|
+
readonly appId: Schema.refine<import("./index.js").AppId, Schema.String>;
|
|
17
|
+
readonly publicSurfaceId: Schema.refine<import("./index.js").PublicSurfaceId, Schema.String>;
|
|
18
|
+
readonly stableRoute: Schema.refine<import("./index.js").StableRoute, Schema.String>;
|
|
19
|
+
readonly releaseId: Schema.refine<import("./index.js").ReleaseId, Schema.String>;
|
|
20
|
+
}>;
|
|
21
|
+
export type OwnedPublicRoute = Schema.Schema.Type<typeof ownedPublicRouteSchema>;
|
|
22
|
+
/** Published customer Surfaces still held by this Team, with their current promotion or rollback. */
|
|
23
|
+
export declare function listD1OwnedPublicRoutes(database: AppReleaseD1Database, teamId: string): Promise<readonly OwnedPublicRoute[]>;
|
|
24
|
+
/** Catalog-local irreversible closure; public admission never depends on Global availability. */
|
|
25
|
+
export declare function closedD1PublicRoute(input: {
|
|
26
|
+
catalog: AppReleaseD1Database;
|
|
27
|
+
route: string;
|
|
28
|
+
}): Promise<boolean>;
|
|
29
|
+
/** A platform artifact keeps its public mount; a customer claim never qualifies. */
|
|
30
|
+
export declare function platformD1PublicRoute(database: AppReleaseD1Database, input: {
|
|
31
|
+
appId: string;
|
|
32
|
+
stableRoute: string;
|
|
33
|
+
}): Promise<boolean>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { appIdSchema, publicSurfaceIdSchema, releaseIdSchema, stableRouteSchema, } from "./internal/app-release-schema.js";
|
|
3
|
+
import { capturePublicRoutePolicy, publicRouteKey, publicRoutePolicy, } from "./public-route-policy.js";
|
|
4
|
+
const policyRowSchema = Schema.Struct({
|
|
5
|
+
version: Schema.Number,
|
|
6
|
+
digest: Schema.String,
|
|
7
|
+
});
|
|
8
|
+
/** Fail closed until the operator has installed this exact deployment policy. */
|
|
9
|
+
export async function publicRoutePolicyReady(database, policy) {
|
|
10
|
+
const row = await database
|
|
11
|
+
.prepare("SELECT version,digest FROM app_release_route_policy WHERE singleton=1")
|
|
12
|
+
.first();
|
|
13
|
+
if (row === null)
|
|
14
|
+
return false;
|
|
15
|
+
const found = Schema.decodeUnknownSync(policyRowSchema)(row);
|
|
16
|
+
return found.version === policy.version && found.digest === policy.digest;
|
|
17
|
+
}
|
|
18
|
+
/** Operator composition only; values come from its captured deployment, not a request. */
|
|
19
|
+
export async function configureD1PublicRoutes(database, policy) {
|
|
20
|
+
if (await publicRoutePolicyReady(database, policy))
|
|
21
|
+
return;
|
|
22
|
+
const exact = new Map();
|
|
23
|
+
for (const word of [
|
|
24
|
+
...publicRoutePolicy.reservedWords,
|
|
25
|
+
...policy.configuration.structuralRoutes,
|
|
26
|
+
])
|
|
27
|
+
exact.set(publicRouteKey(word), word);
|
|
28
|
+
const statements = [
|
|
29
|
+
database.prepare("DELETE FROM app_release_reserved_route_keys"),
|
|
30
|
+
database.prepare("DELETE FROM app_release_route_brand_exceptions"),
|
|
31
|
+
database.prepare("DELETE FROM app_release_platform_routes"),
|
|
32
|
+
...[...exact].map(([key, word]) => database
|
|
33
|
+
.prepare("INSERT INTO app_release_reserved_route_keys(word,route_key,match_kind) VALUES (?,?,'exact')")
|
|
34
|
+
.bind(word, key)),
|
|
35
|
+
...publicRoutePolicy.brandPrefixes.map((word) => database
|
|
36
|
+
.prepare("INSERT INTO app_release_reserved_route_keys(word,route_key,match_kind) VALUES (?,?,'prefix')")
|
|
37
|
+
.bind(word, publicRouteKey(word))),
|
|
38
|
+
...publicRoutePolicy.brandWordExceptions.map((word) => database
|
|
39
|
+
.prepare("INSERT INTO app_release_route_brand_exceptions(word,route_key) VALUES (?,?)")
|
|
40
|
+
.bind(word, publicRouteKey(word))),
|
|
41
|
+
...policy.configuration.platformRoutes.map(({ appId, stableRoute }) => database
|
|
42
|
+
.prepare("INSERT INTO app_release_platform_routes(stable_route,route_key,app_id) VALUES (?,?,?)")
|
|
43
|
+
.bind(stableRoute, publicRouteKey(stableRoute), appId)),
|
|
44
|
+
database
|
|
45
|
+
.prepare(`INSERT INTO app_release_route_policy
|
|
46
|
+
(singleton,version,digest,config_json,max_team_routes,max_unused_surfaces_per_app)
|
|
47
|
+
VALUES (1,?,?,?,?,?) ON CONFLICT(singleton) DO UPDATE SET
|
|
48
|
+
version=excluded.version,digest=excluded.digest,config_json=excluded.config_json,
|
|
49
|
+
max_team_routes=excluded.max_team_routes,max_unused_surfaces_per_app=excluded.max_unused_surfaces_per_app`)
|
|
50
|
+
.bind(policy.version, policy.digest, policy.json, publicRoutePolicy.maxPublicRoutesPerTeam, publicRoutePolicy.maxUnusedSurfacesPerApp),
|
|
51
|
+
];
|
|
52
|
+
const result = await database.batch(statements);
|
|
53
|
+
if (result.length !== statements.length || result.some((row) => !row.success))
|
|
54
|
+
throw new Error("Public route policy projection unavailable");
|
|
55
|
+
}
|
|
56
|
+
const heldClaimSchema = Schema.Struct({
|
|
57
|
+
app_id: Schema.String,
|
|
58
|
+
public_surface_id: Schema.String,
|
|
59
|
+
team_id: Schema.String,
|
|
60
|
+
user_id: Schema.String,
|
|
61
|
+
stable_route: Schema.String,
|
|
62
|
+
});
|
|
63
|
+
const sellerRouteJoins = `FROM app_release_route_claims c
|
|
64
|
+
JOIN app_release_public_surfaces s ON s.public_surface_id=c.public_surface_id
|
|
65
|
+
JOIN app_release_team_publishers p ON p.app_id=s.app_id AND p.team_id=c.team_id`;
|
|
66
|
+
const sellerRouteCustody = `c.state='held' AND s.authority_kind='customer'
|
|
67
|
+
AND p.source_provider='artifacts' AND p.source_repository=p.app_id
|
|
68
|
+
AND p.template_app_id IS NOT NULL AND p.template_app_id<>p.app_id
|
|
69
|
+
AND p.template_seed_revision IS NOT NULL
|
|
70
|
+
AND NOT EXISTS (SELECT 1 FROM app_release_surface_retirements r WHERE r.public_surface_id=c.public_surface_id)
|
|
71
|
+
AND NOT EXISTS (SELECT 1 FROM app_release_team_route_blocks b WHERE b.team_id=c.team_id AND b.route_key=c.route_key)`;
|
|
72
|
+
/** Live seller custody; a buyer entitlement or a historical Surface is never ownership. */
|
|
73
|
+
export async function ownsD1PublicRoute(database, input) {
|
|
74
|
+
const row = await database
|
|
75
|
+
.prepare(`SELECT s.app_id,c.public_surface_id,c.team_id,c.user_id,c.stable_route
|
|
76
|
+
${sellerRouteJoins}
|
|
77
|
+
WHERE c.route_key=? AND c.stable_route=? AND c.team_id=? AND s.app_id=?
|
|
78
|
+
AND ${sellerRouteCustody}`)
|
|
79
|
+
.bind(publicRouteKey(input.stableRoute), input.stableRoute, input.teamId, input.appId)
|
|
80
|
+
.first();
|
|
81
|
+
if (row === null)
|
|
82
|
+
return false;
|
|
83
|
+
Schema.decodeUnknownSync(heldClaimSchema)(row);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const ownedPublicRouteSchema = Schema.Struct({
|
|
87
|
+
appId: appIdSchema,
|
|
88
|
+
publicSurfaceId: publicSurfaceIdSchema,
|
|
89
|
+
stableRoute: stableRouteSchema,
|
|
90
|
+
releaseId: releaseIdSchema,
|
|
91
|
+
});
|
|
92
|
+
/** Published customer Surfaces still held by this Team, with their current promotion or rollback. */
|
|
93
|
+
export async function listD1OwnedPublicRoutes(database, teamId) {
|
|
94
|
+
const { results } = await database
|
|
95
|
+
.prepare(`SELECT s.app_id AS appId,c.public_surface_id AS publicSurfaceId,
|
|
96
|
+
c.stable_route AS stableRoute,t.release_id AS releaseId
|
|
97
|
+
${sellerRouteJoins}
|
|
98
|
+
JOIN app_release_transitions t ON t.public_surface_id=c.public_surface_id
|
|
99
|
+
WHERE c.team_id=? AND ${sellerRouteCustody}
|
|
100
|
+
AND t.operation_kind IN ('promotion','rollback')
|
|
101
|
+
AND t.sequence=(SELECT MAX(latest.sequence) FROM app_release_transitions latest
|
|
102
|
+
WHERE latest.public_surface_id=c.public_surface_id AND latest.operation_kind IN ('promotion','rollback'))
|
|
103
|
+
ORDER BY s.app_id,c.public_surface_id`)
|
|
104
|
+
.bind(teamId)
|
|
105
|
+
.all();
|
|
106
|
+
return Schema.decodeUnknownSync(Schema.Array(ownedPublicRouteSchema))(results);
|
|
107
|
+
}
|
|
108
|
+
/** Catalog-local irreversible closure; public admission never depends on Global availability. */
|
|
109
|
+
export async function closedD1PublicRoute(input) {
|
|
110
|
+
const row = await input.catalog
|
|
111
|
+
.prepare(`SELECT c.team_id FROM app_release_route_claims c
|
|
112
|
+
JOIN app_release_public_surfaces s ON s.public_surface_id=c.public_surface_id
|
|
113
|
+
JOIN app_release_closed_sellers closed ON closed.team_id=c.team_id
|
|
114
|
+
WHERE c.route_key=? AND c.stable_route=? AND c.state='held' AND s.authority_kind='customer'`)
|
|
115
|
+
.bind(publicRouteKey(input.route), input.route)
|
|
116
|
+
.first();
|
|
117
|
+
if (row === null)
|
|
118
|
+
return false;
|
|
119
|
+
Schema.decodeUnknownSync(Schema.Struct({ team_id: Schema.String }))(row);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/** A platform artifact keeps its public mount; a customer claim never qualifies. */
|
|
123
|
+
export async function platformD1PublicRoute(database, input) {
|
|
124
|
+
const row = await database
|
|
125
|
+
.prepare(`SELECT s.app_id FROM app_release_public_surfaces s
|
|
126
|
+
JOIN app_release_platform_routes p ON p.app_id=s.app_id AND p.stable_route=s.stable_route
|
|
127
|
+
JOIN app_release_route_claims c ON c.public_surface_id=s.public_surface_id
|
|
128
|
+
WHERE s.app_id=? AND s.stable_route=? AND c.route_key=? AND c.stable_route=s.stable_route
|
|
129
|
+
AND c.state='held' AND s.authority_kind='platform'`)
|
|
130
|
+
.bind(input.appId, input.stableRoute, publicRouteKey(input.stableRoute))
|
|
131
|
+
.first();
|
|
132
|
+
if (row === null)
|
|
133
|
+
return false;
|
|
134
|
+
Schema.decodeUnknownSync(Schema.Struct({ app_id: Schema.String }))(row);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { AppId, SourceRevision } from "./internal/app-release-contract.js";
|
|
3
|
+
/** Canonical source repository resolved by the product publisher. */
|
|
4
|
+
export declare const appSourceRepositorySchema: Schema.Struct<{
|
|
5
|
+
readonly provider: Schema.Literals<readonly ["artifacts", "github"]>;
|
|
6
|
+
readonly repository: Schema.String;
|
|
7
|
+
readonly branch: Schema.Literal<"main">;
|
|
8
|
+
}>;
|
|
9
|
+
/** Team-owned publication authority for one separately registered App. */
|
|
10
|
+
export declare const teamAppPublisherRegistrationSchema: Schema.Struct<{
|
|
11
|
+
readonly appId: Schema.refine<AppId, Schema.String>;
|
|
12
|
+
readonly teamId: Schema.String;
|
|
13
|
+
readonly source: Schema.Struct<{
|
|
14
|
+
readonly provider: Schema.Literals<readonly ["artifacts", "github"]>;
|
|
15
|
+
readonly repository: Schema.String;
|
|
16
|
+
readonly branch: Schema.Literal<"main">;
|
|
17
|
+
}>;
|
|
18
|
+
readonly templateSeed: Schema.optionalKey<Schema.Struct<{
|
|
19
|
+
readonly appId: Schema.refine<AppId, Schema.String>;
|
|
20
|
+
readonly revision: Schema.refine<SourceRevision, Schema.String>;
|
|
21
|
+
}>>;
|
|
22
|
+
}>;
|
|
23
|
+
/** Exact first-party platform seed from which a Team-owned App was forked. */
|
|
24
|
+
export type TeamAppTemplateSeed = Readonly<{
|
|
25
|
+
appId: AppId;
|
|
26
|
+
revision: SourceRevision;
|
|
27
|
+
}>;
|
|
28
|
+
/** Parsed immutable source location used to build one App. */
|
|
29
|
+
export type AppSourceRepository = Schema.Schema.Type<typeof appSourceRepositorySchema>;
|
|
30
|
+
/** Parsed Team ownership record for one App publisher. */
|
|
31
|
+
export type TeamAppPublisherRegistration = Schema.Schema.Type<typeof teamAppPublisherRegistrationSchema>;
|
|
32
|
+
/** Result of reading the durable publisher registered for an App. */
|
|
33
|
+
export type TeamAppPublisherRegistrationRead = Readonly<{
|
|
34
|
+
kind: "found";
|
|
35
|
+
registration: TeamAppPublisherRegistration;
|
|
36
|
+
}> | Readonly<{
|
|
37
|
+
kind: "absent";
|
|
38
|
+
}>;
|
|
39
|
+
/** Idempotent result of registering one App publisher. */
|
|
40
|
+
export type TeamAppPublisherRegistrationCreate = Readonly<{
|
|
41
|
+
kind: "created" | "existing";
|
|
42
|
+
registration: TeamAppPublisherRegistration;
|
|
43
|
+
}> | Readonly<{
|
|
44
|
+
kind: "conflict";
|
|
45
|
+
}>;
|
|
46
|
+
/** Team-scoped decision for publishing a registered App. */
|
|
47
|
+
export type TeamAppPublicationAuthorization = Readonly<{
|
|
48
|
+
kind: "authorized";
|
|
49
|
+
registration: TeamAppPublisherRegistration;
|
|
50
|
+
}> | Readonly<{
|
|
51
|
+
kind: "absent" | "denied";
|
|
52
|
+
}>;
|
|
53
|
+
/** Durable App publisher facts; Team membership remains owned by Hub. */
|
|
54
|
+
export type TeamAppPublisherRegistry = Readonly<{
|
|
55
|
+
/** Read the publisher registration for an App. */
|
|
56
|
+
read(appId: AppId): Promise<TeamAppPublisherRegistrationRead>;
|
|
57
|
+
/** Atomically create or observe one immutable publisher registration. */
|
|
58
|
+
register(registration: TeamAppPublisherRegistration): Promise<TeamAppPublisherRegistrationCreate>;
|
|
59
|
+
/** Authorize a Team against the App's retained publisher registration. */
|
|
60
|
+
authorize(appId: AppId, teamId: string): Promise<TeamAppPublicationAuthorization>;
|
|
61
|
+
}>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { appIdSchema, sourceRevisionSchema, } from "./internal/app-release-schema.js";
|
|
3
|
+
const boundedIdentity = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
|
|
4
|
+
/** Canonical source repository resolved by the product publisher. */
|
|
5
|
+
export const appSourceRepositorySchema = Schema.Struct({
|
|
6
|
+
provider: Schema.Literals(["artifacts", "github"]),
|
|
7
|
+
repository: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(1024))),
|
|
8
|
+
branch: Schema.Literal("main"),
|
|
9
|
+
});
|
|
10
|
+
/** Team-owned publication authority for one separately registered App. */
|
|
11
|
+
export const teamAppPublisherRegistrationSchema = Schema.Struct({
|
|
12
|
+
appId: appIdSchema,
|
|
13
|
+
teamId: boundedIdentity,
|
|
14
|
+
source: appSourceRepositorySchema,
|
|
15
|
+
templateSeed: Schema.optionalKey(Schema.Struct({ appId: appIdSchema, revision: sourceRevisionSchema })),
|
|
16
|
+
});
|