@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,56 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { AppReleaseD1Database } from "./cloudflare.js";
|
|
3
|
+
declare const safetyTakedownSchema: Schema.Struct<{
|
|
4
|
+
readonly operationId: Schema.String;
|
|
5
|
+
readonly operatorId: Schema.String;
|
|
6
|
+
readonly appId: Schema.refine<import("./index.js").AppId, Schema.String>;
|
|
7
|
+
readonly stableRoute: Schema.refine<import("./index.js").StableRoute, Schema.String>;
|
|
8
|
+
readonly reason: Schema.String;
|
|
9
|
+
}>;
|
|
10
|
+
export type SafetyTakedown = Schema.Schema.Type<typeof safetyTakedownSchema>;
|
|
11
|
+
/** Audited intent precedes the maintained revocation path; a pending action is discoverable and retryable. */
|
|
12
|
+
export declare function createD1SafetyTakedowns(database: AppReleaseD1Database, now?: () => number): {
|
|
13
|
+
prepare: (raw: SafetyTakedown) => Promise<{
|
|
14
|
+
kind: "conflict";
|
|
15
|
+
operation?: never;
|
|
16
|
+
releaseIds?: never;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "recorded";
|
|
19
|
+
operation: {
|
|
20
|
+
readonly operation_id: string;
|
|
21
|
+
readonly app_id: import("./index.js").AppId;
|
|
22
|
+
readonly stable_route: import("./index.js").StableRoute;
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
readonly operator_id: string;
|
|
25
|
+
readonly recorded_at: number;
|
|
26
|
+
readonly state: "complete" | "pending";
|
|
27
|
+
readonly completed_at: number | null;
|
|
28
|
+
};
|
|
29
|
+
releaseIds: import("./index.js").ReleaseId[];
|
|
30
|
+
} | {
|
|
31
|
+
kind: "unavailable";
|
|
32
|
+
operation?: never;
|
|
33
|
+
releaseIds?: never;
|
|
34
|
+
}>;
|
|
35
|
+
complete: (operationId: string, operatorId: string) => Promise<{
|
|
36
|
+
kind: "conflict";
|
|
37
|
+
} | {
|
|
38
|
+
kind: "recorded";
|
|
39
|
+
}>;
|
|
40
|
+
pending: () => Promise<{
|
|
41
|
+
readonly operation_id: string;
|
|
42
|
+
readonly app_id: import("./index.js").AppId;
|
|
43
|
+
readonly stable_route: import("./index.js").StableRoute;
|
|
44
|
+
readonly reason: string;
|
|
45
|
+
readonly operator_id: string;
|
|
46
|
+
readonly recorded_at: number;
|
|
47
|
+
readonly state: "complete" | "pending";
|
|
48
|
+
readonly completed_at: number | null;
|
|
49
|
+
}[]>;
|
|
50
|
+
capturedReleasePage: (operationId: string, after?: string) => Promise<{
|
|
51
|
+
releaseIds: import("./index.js").ReleaseId[];
|
|
52
|
+
next: import("./index.js").ReleaseId | null;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
export declare const safetyTakedownSchemaSql: string;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { appIdSchema, releaseIdSchema, stableRouteSchema, } from "./internal/app-release-schema.js";
|
|
3
|
+
import { reportFailure } from "./internal/failure.js";
|
|
4
|
+
const identity = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(128)));
|
|
5
|
+
const safetyTakedownSchema = Schema.Struct({
|
|
6
|
+
operationId: identity,
|
|
7
|
+
operatorId: identity,
|
|
8
|
+
appId: appIdSchema,
|
|
9
|
+
stableRoute: stableRouteSchema,
|
|
10
|
+
reason: Schema.String.pipe(Schema.check(Schema.isPattern(/\S/u)), Schema.check(Schema.isMaxLength(1000))),
|
|
11
|
+
});
|
|
12
|
+
const operationSchema = Schema.Struct({
|
|
13
|
+
operation_id: identity,
|
|
14
|
+
app_id: appIdSchema,
|
|
15
|
+
stable_route: stableRouteSchema,
|
|
16
|
+
reason: Schema.String,
|
|
17
|
+
operator_id: identity,
|
|
18
|
+
recorded_at: Schema.Number,
|
|
19
|
+
state: Schema.Literals(["pending", "complete"]),
|
|
20
|
+
completed_at: Schema.NullOr(Schema.Number),
|
|
21
|
+
});
|
|
22
|
+
/** Audited intent precedes the maintained revocation path; a pending action is discoverable and retryable. */
|
|
23
|
+
export function createD1SafetyTakedowns(database, now = Date.now) {
|
|
24
|
+
const read = async (operationId) => {
|
|
25
|
+
const row = await database
|
|
26
|
+
.prepare("SELECT * FROM app_release_safety_takedowns WHERE operation_id=?")
|
|
27
|
+
.bind(operationId)
|
|
28
|
+
.first();
|
|
29
|
+
return row === null ? null : Schema.decodeUnknownSync(operationSchema)(row);
|
|
30
|
+
};
|
|
31
|
+
const releasePage = async (operationId) => {
|
|
32
|
+
const rows = await database
|
|
33
|
+
.prepare(`SELECT r.release_id FROM app_release_safety_takedown_releases r
|
|
34
|
+
WHERE r.operation_id=? AND NOT EXISTS (
|
|
35
|
+
SELECT 1 FROM app_release_transitions t JOIN app_release_transition_obligations o USING(sequence)
|
|
36
|
+
WHERE t.release_id=r.release_id AND t.operation_kind='revocation' AND o.state='acknowledged')
|
|
37
|
+
ORDER BY r.release_id LIMIT 1000`)
|
|
38
|
+
.bind(operationId)
|
|
39
|
+
.all();
|
|
40
|
+
return rows.results.map((row) => Schema.decodeUnknownSync(Schema.Struct({ release_id: releaseIdSchema }))(row).release_id);
|
|
41
|
+
};
|
|
42
|
+
const capturedReleasePage = async (operationId, after = "") => {
|
|
43
|
+
Schema.decodeUnknownSync(identity)(operationId);
|
|
44
|
+
Schema.decodeUnknownSync(Schema.Union([Schema.Literal(""), releaseIdSchema]))(after);
|
|
45
|
+
const rows = await database
|
|
46
|
+
.prepare(`SELECT release_id FROM app_release_safety_takedown_releases
|
|
47
|
+
WHERE operation_id=? AND release_id>? ORDER BY release_id LIMIT 1001`)
|
|
48
|
+
.bind(operationId, after)
|
|
49
|
+
.all();
|
|
50
|
+
const ids = rows.results.map((row) => Schema.decodeUnknownSync(Schema.Struct({ release_id: releaseIdSchema }))(row).release_id);
|
|
51
|
+
return {
|
|
52
|
+
releaseIds: ids.slice(0, 1000),
|
|
53
|
+
next: ids.length > 1000 ? (ids[999] ?? null) : null,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const prepare = async (raw) => {
|
|
57
|
+
try {
|
|
58
|
+
const input = Schema.decodeUnknownSync(safetyTakedownSchema, {
|
|
59
|
+
onExcessProperty: "error",
|
|
60
|
+
})(raw);
|
|
61
|
+
// Snapshot all Releases in child rows; bounded delivery pages no longer cap an App's history.
|
|
62
|
+
const recipients = `SELECT t.release_id FROM app_release_transitions t JOIN app_release_public_surfaces s USING(public_surface_id)
|
|
63
|
+
WHERE s.app_id=? AND s.stable_route=? AND t.operation_kind IN ('promotion','rollback')
|
|
64
|
+
UNION SELECT release_id FROM app_release_buyer_references WHERE app_id=? AND stable_route=?`;
|
|
65
|
+
const results = await database.batch([
|
|
66
|
+
database
|
|
67
|
+
.prepare(`INSERT INTO app_release_safety_takedowns(operation_id,app_id,stable_route,reason,operator_id,recorded_at,state)
|
|
68
|
+
SELECT ?,?,?,?,?,?,'pending' WHERE EXISTS (${recipients}) ON CONFLICT DO NOTHING`)
|
|
69
|
+
.bind(input.operationId, input.appId, input.stableRoute, input.reason, input.operatorId, now(), input.appId, input.stableRoute, input.appId, input.stableRoute),
|
|
70
|
+
database
|
|
71
|
+
.prepare(`INSERT INTO app_release_safety_takedown_releases(operation_id,release_id)
|
|
72
|
+
SELECT ?,r.release_id FROM (${recipients}) r
|
|
73
|
+
WHERE EXISTS (SELECT 1 FROM app_release_safety_takedowns a WHERE a.operation_id=? AND a.app_id=? AND a.stable_route=? AND a.reason=?)
|
|
74
|
+
AND NOT EXISTS (SELECT 1 FROM app_release_safety_takedown_releases frozen WHERE frozen.operation_id=?)
|
|
75
|
+
ON CONFLICT DO NOTHING`)
|
|
76
|
+
.bind(input.operationId, input.appId, input.stableRoute, input.appId, input.stableRoute, input.operationId, input.appId, input.stableRoute, input.reason, input.operationId),
|
|
77
|
+
]);
|
|
78
|
+
if (results.some((result) => !result.success))
|
|
79
|
+
throw new Error("Safety snapshot unavailable");
|
|
80
|
+
const saved = await read(input.operationId);
|
|
81
|
+
if (saved?.app_id !== input.appId ||
|
|
82
|
+
saved.stable_route !== input.stableRoute ||
|
|
83
|
+
saved.reason !== input.reason)
|
|
84
|
+
return { kind: "conflict" };
|
|
85
|
+
await database
|
|
86
|
+
.prepare(`INSERT INTO app_release_safety_takedown_steps(operation_id,phase,operator_id,recorded_at)
|
|
87
|
+
VALUES(?,'prepared',?,?)`)
|
|
88
|
+
.bind(input.operationId, input.operatorId, now())
|
|
89
|
+
.run();
|
|
90
|
+
return {
|
|
91
|
+
kind: "recorded",
|
|
92
|
+
operation: saved,
|
|
93
|
+
releaseIds: await releasePage(input.operationId),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
reportFailure("safety takedown preparation", cause);
|
|
98
|
+
return { kind: "unavailable" };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const complete = async (operationId, operatorId) => {
|
|
102
|
+
for (const value of [operationId, operatorId])
|
|
103
|
+
Schema.decodeUnknownSync(identity)(value);
|
|
104
|
+
const operation = await read(operationId);
|
|
105
|
+
if (operation === null)
|
|
106
|
+
return { kind: "conflict" };
|
|
107
|
+
const results = await database.batch([
|
|
108
|
+
database
|
|
109
|
+
.prepare(`INSERT INTO app_release_safety_takedown_steps(operation_id,phase,operator_id,recorded_at)
|
|
110
|
+
SELECT ?,'completed',?,? WHERE EXISTS (SELECT 1 FROM app_release_safety_takedowns a WHERE a.operation_id=? AND a.state IN ('pending','complete')
|
|
111
|
+
AND NOT EXISTS (SELECT 1 FROM app_release_safety_takedown_releases r WHERE r.operation_id=a.operation_id AND NOT EXISTS (
|
|
112
|
+
SELECT 1 FROM app_release_transitions t JOIN app_release_transition_obligations o ON o.sequence=t.sequence
|
|
113
|
+
WHERE t.release_id=r.release_id AND t.operation_kind='revocation' AND o.state='acknowledged')))
|
|
114
|
+
ON CONFLICT DO NOTHING`)
|
|
115
|
+
.bind(operationId, operatorId, now(), operationId),
|
|
116
|
+
database
|
|
117
|
+
.prepare(`UPDATE app_release_safety_takedowns SET state='complete',completed_at=? WHERE operation_id=? AND state='pending'
|
|
118
|
+
AND EXISTS (SELECT 1 FROM app_release_safety_takedown_steps WHERE operation_id=? AND phase='completed')`)
|
|
119
|
+
.bind(now(), operationId, operationId),
|
|
120
|
+
]);
|
|
121
|
+
if (results.some((result) => !result.success))
|
|
122
|
+
throw new Error("Safety audit unavailable");
|
|
123
|
+
return (await read(operationId))?.state === "complete"
|
|
124
|
+
? { kind: "recorded" }
|
|
125
|
+
: { kind: "conflict" };
|
|
126
|
+
};
|
|
127
|
+
const pending = async () => {
|
|
128
|
+
const result = await database
|
|
129
|
+
.prepare("SELECT * FROM app_release_safety_takedowns WHERE state='pending' ORDER BY operation_id LIMIT 100")
|
|
130
|
+
.all();
|
|
131
|
+
return result.results.map((row) => Schema.decodeUnknownSync(operationSchema)(row));
|
|
132
|
+
};
|
|
133
|
+
return { prepare, complete, pending, capturedReleasePage };
|
|
134
|
+
}
|
|
135
|
+
export const safetyTakedownSchemaSql = [
|
|
136
|
+
`CREATE TABLE app_release_safety_takedowns(operation_id TEXT PRIMARY KEY,app_id TEXT NOT NULL,stable_route TEXT NOT NULL,
|
|
137
|
+
reason TEXT NOT NULL CHECK(length(reason) BETWEEN 1 AND 1000),operator_id TEXT NOT NULL,recorded_at INTEGER NOT NULL,
|
|
138
|
+
state TEXT NOT NULL CHECK(state IN ('pending','complete')),completed_at INTEGER) STRICT;`,
|
|
139
|
+
`CREATE TABLE app_release_safety_takedown_releases(operation_id TEXT NOT NULL REFERENCES app_release_safety_takedowns(operation_id),release_id TEXT NOT NULL,PRIMARY KEY(operation_id,release_id)) STRICT;`,
|
|
140
|
+
`CREATE INDEX app_release_safety_takedowns_pending ON app_release_safety_takedowns(state,operation_id);`,
|
|
141
|
+
`CREATE TABLE app_release_safety_takedown_steps(step_id INTEGER PRIMARY KEY,operation_id TEXT NOT NULL REFERENCES app_release_safety_takedowns(operation_id),
|
|
142
|
+
phase TEXT NOT NULL CHECK(phase IN ('prepared','completed')),operator_id TEXT NOT NULL,recorded_at INTEGER NOT NULL) STRICT;`,
|
|
143
|
+
`CREATE INDEX app_release_safety_takedown_steps_by_action ON app_release_safety_takedown_steps(operation_id,phase);`,
|
|
144
|
+
]
|
|
145
|
+
.map((statement) => statement.replaceAll("\n", " "))
|
|
146
|
+
.join("\n");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/** A retained entitlement continues after its seller closes; this is not a revocation. */
|
|
3
|
+
export declare const sellerClosureNoticeSchema: Schema.Struct<{
|
|
4
|
+
readonly kind: Schema.Literal<"seller-closed">;
|
|
5
|
+
readonly dispositionId: Schema.String;
|
|
6
|
+
readonly sellerAccountId: Schema.String;
|
|
7
|
+
readonly buyerAccountId: Schema.String;
|
|
8
|
+
readonly appId: Schema.String;
|
|
9
|
+
readonly releaseIds: Schema.Tuple<readonly [Schema.String]>;
|
|
10
|
+
readonly displayName: Schema.String;
|
|
11
|
+
readonly sellerName: Schema.String;
|
|
12
|
+
readonly requestId: Schema.String;
|
|
13
|
+
}>;
|
|
14
|
+
export type SellerClosureNotice = Schema.Schema.Type<typeof sellerClosureNoticeSchema>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const identity = Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(256)));
|
|
3
|
+
/** A retained entitlement continues after its seller closes; this is not a revocation. */
|
|
4
|
+
export const sellerClosureNoticeSchema = Schema.Struct({
|
|
5
|
+
kind: Schema.Literal("seller-closed"),
|
|
6
|
+
dispositionId: Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(1024))),
|
|
7
|
+
sellerAccountId: identity,
|
|
8
|
+
buyerAccountId: identity,
|
|
9
|
+
appId: identity,
|
|
10
|
+
releaseIds: Schema.Tuple([identity]),
|
|
11
|
+
displayName: identity,
|
|
12
|
+
sellerName: identity,
|
|
13
|
+
requestId: identity,
|
|
14
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { AppReleaseD1Database } from "./cloudflare.js";
|
|
3
|
+
declare const sellerClosureSchema: Schema.Struct<{
|
|
4
|
+
readonly teamId: Schema.String;
|
|
5
|
+
readonly requestId: Schema.String;
|
|
6
|
+
readonly closedAt: Schema.Number;
|
|
7
|
+
}>;
|
|
8
|
+
export type SellerClosure = Schema.Schema.Type<typeof sellerClosureSchema>;
|
|
9
|
+
export declare class SellerClosureConflictError extends Error {
|
|
10
|
+
readonly code = "seller_closure_conflict";
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
/** Trusted close-decision delivery only. Keep the original irreversible decision on retries. */
|
|
14
|
+
export declare function recordD1SellerClosure(database: AppReleaseD1Database, raw: SellerClosure): Promise<void>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const sellerClosureSchema = Schema.Struct({
|
|
3
|
+
teamId: Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(256))),
|
|
4
|
+
requestId: Schema.String.pipe(Schema.check(Schema.isMinLength(1), Schema.isMaxLength(256))),
|
|
5
|
+
closedAt: Schema.Number.pipe(Schema.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }))),
|
|
6
|
+
});
|
|
7
|
+
export class SellerClosureConflictError extends Error {
|
|
8
|
+
code = "seller_closure_conflict";
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Seller closure marker conflicts with the original decision");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Trusted close-decision delivery only. Keep the original irreversible decision on retries. */
|
|
14
|
+
export async function recordD1SellerClosure(database, raw) {
|
|
15
|
+
const input = Schema.decodeUnknownSync(sellerClosureSchema, {
|
|
16
|
+
onExcessProperty: "error",
|
|
17
|
+
})(raw);
|
|
18
|
+
const result = await database
|
|
19
|
+
.prepare("INSERT INTO app_release_closed_sellers(team_id,request_id,closed_at) VALUES (?,?,?) ON CONFLICT(team_id) DO NOTHING")
|
|
20
|
+
.bind(input.teamId, input.requestId, input.closedAt)
|
|
21
|
+
.run();
|
|
22
|
+
if (!result.success)
|
|
23
|
+
throw new Error("Seller closure marker unavailable");
|
|
24
|
+
const row = await database
|
|
25
|
+
.prepare("SELECT team_id AS teamId,request_id AS requestId,closed_at AS closedAt FROM app_release_closed_sellers WHERE team_id=?")
|
|
26
|
+
.bind(input.teamId)
|
|
27
|
+
.first();
|
|
28
|
+
const saved = Schema.decodeUnknownSync(sellerClosureSchema)(row);
|
|
29
|
+
if (saved.requestId !== input.requestId || saved.closedAt !== input.closedAt)
|
|
30
|
+
throw new SellerClosureConflictError();
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { AppReleaseD1Database } from "./cloudflare.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adapt caller-owned Node SQLite to the catalog's D1 transport for tests and
|
|
5
|
+
* quick starts. The caller applies schema and closes the database. This is not
|
|
6
|
+
* a Cloudflare emulator or an isolation boundary.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sqliteD1(sqlite: DatabaseSync): AppReleaseD1Database;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { watchdogSqliteValueSchema, } from "@fungi.computer/watchdog";
|
|
3
|
+
import { fromNodeSqlite } from "@fungi.computer/watchdog/node-sqlite";
|
|
4
|
+
const decodeBindings = Schema.decodeUnknownSync(Schema.Array(watchdogSqliteValueSchema));
|
|
5
|
+
const decodeChanges = Schema.decodeUnknownSync(Schema.Struct({ changes: Schema.Number }));
|
|
6
|
+
/**
|
|
7
|
+
* Adapt caller-owned Node SQLite to the catalog's D1 transport for tests and
|
|
8
|
+
* quick starts. The caller applies schema and closes the database. This is not
|
|
9
|
+
* a Cloudflare emulator or an isolation boundary.
|
|
10
|
+
*/
|
|
11
|
+
export function sqliteD1(sqlite) {
|
|
12
|
+
const owner = fromNodeSqlite(sqlite);
|
|
13
|
+
const executions = new WeakMap();
|
|
14
|
+
const prepare = (sql, values = []) => {
|
|
15
|
+
const execute = () => {
|
|
16
|
+
const before = decodeChanges(owner.sql.exec("SELECT total_changes() AS changes").one()).changes;
|
|
17
|
+
const results = owner.sql.exec(sql, ...values).toArray();
|
|
18
|
+
const after = decodeChanges(owner.sql.exec("SELECT total_changes() AS changes").one()).changes;
|
|
19
|
+
const { changes } = decodeChanges(owner.sql.exec("SELECT changes() AS changes").one());
|
|
20
|
+
return {
|
|
21
|
+
success: true,
|
|
22
|
+
results,
|
|
23
|
+
meta: { changes: after === before ? 0 : changes },
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const statement = {
|
|
27
|
+
bind: (...next) => prepare(sql, decodeBindings(next)),
|
|
28
|
+
first: () => Promise.resolve().then(() => owner.sql.exec(sql, ...values).toArray()[0] ?? null),
|
|
29
|
+
all: () => Promise.resolve().then(() => ({
|
|
30
|
+
success: true,
|
|
31
|
+
results: owner.sql.exec(sql, ...values).toArray(),
|
|
32
|
+
})),
|
|
33
|
+
run: () => Promise.resolve().then(execute),
|
|
34
|
+
};
|
|
35
|
+
executions.set(statement, execute);
|
|
36
|
+
return statement;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
prepare,
|
|
40
|
+
batch: (statements) => Promise.resolve().then(() => owner.transactionSync(() => statements.map((statement) => {
|
|
41
|
+
const execute = executions.get(statement);
|
|
42
|
+
if (execute === undefined)
|
|
43
|
+
throw new Error("Statement belongs to a different database");
|
|
44
|
+
return execute();
|
|
45
|
+
}))),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
|
|
2
|
+
|
|
3
|
+
**@fungi.computer/puffball**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @fungi.computer/puffball
|
|
8
|
+
|
|
9
|
+
## Modules
|
|
10
|
+
|
|
11
|
+
| Module | Description |
|
|
12
|
+
| ------------------------------------------------------------------- | ----------- |
|
|
13
|
+
| [app-backend-binding-vocabulary](app-backend-binding-vocabulary.md) | - |
|
|
14
|
+
| [app-backend-bindings](app-backend-bindings.md) | - |
|
|
15
|
+
| [app-event-facts](app-event-facts.md) | - |
|
|
16
|
+
| [app-method-context](app-method-context.md) | - |
|
|
17
|
+
| [app-method-runtime](app-method-runtime.md) | - |
|
|
18
|
+
| [backend](backend.md) | - |
|
|
19
|
+
| [background-replay](background-replay.md) | - |
|
|
20
|
+
| [cloudflare](cloudflare.md) | - |
|
|
21
|
+
| [effect](effect.md) | - |
|
|
22
|
+
| [git-representation](git-representation.md) | - |
|
|
23
|
+
| [inbox](inbox.md) | - |
|
|
24
|
+
| [index](index.md) | - |
|
|
25
|
+
| [publisher](publisher.md) | - |
|
|
26
|
+
| [testing](testing.md) | - |
|