@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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +443 -2
  3. package/dist/app-api.d.ts +7 -0
  4. package/dist/app-api.js +61 -0
  5. package/dist/app-backend-binding-vocabulary.d.ts +299 -0
  6. package/dist/app-backend-binding-vocabulary.js +257 -0
  7. package/dist/app-backend-bindings.d.ts +21 -0
  8. package/dist/app-backend-bindings.js +47 -0
  9. package/dist/app-declaration.d.ts +46 -0
  10. package/dist/app-declaration.js +115 -0
  11. package/dist/app-event-facts.d.ts +40 -0
  12. package/dist/app-event-facts.js +28 -0
  13. package/dist/app-method-context.d.ts +109 -0
  14. package/dist/app-method-context.js +9 -0
  15. package/dist/app-method-runtime.d.ts +86 -0
  16. package/dist/app-method-runtime.js +122 -0
  17. package/dist/app-origin-paths.d.ts +6 -0
  18. package/dist/app-origin-paths.js +6 -0
  19. package/dist/app-vocabulary.d.ts +189 -0
  20. package/dist/app-vocabulary.js +103 -0
  21. package/dist/app.d.ts +379 -0
  22. package/dist/app.js +71 -0
  23. package/dist/artifact-dispositions.d.ts +115 -0
  24. package/dist/artifact-dispositions.js +234 -0
  25. package/dist/assets.d.ts +54 -0
  26. package/dist/assets.js +89 -0
  27. package/dist/backend.d.ts +41 -0
  28. package/dist/backend.js +29 -0
  29. package/dist/background-replay.d.ts +115 -0
  30. package/dist/background-replay.js +175 -0
  31. package/dist/buyer-references.d.ts +69 -0
  32. package/dist/buyer-references.js +154 -0
  33. package/dist/cloudflare-assets.d.ts +48 -0
  34. package/dist/cloudflare-assets.js +105 -0
  35. package/dist/cloudflare.d.ts +254 -0
  36. package/dist/cloudflare.js +1619 -0
  37. package/dist/effect.d.ts +73 -0
  38. package/dist/effect.js +325 -0
  39. package/dist/git-representation.d.ts +13 -0
  40. package/dist/git-representation.js +16 -0
  41. package/dist/inbox.d.ts +132 -0
  42. package/dist/inbox.js +81 -0
  43. package/dist/index.d.ts +31 -0
  44. package/dist/index.js +85 -0
  45. package/dist/internal/app-description.d.ts +14 -0
  46. package/dist/internal/app-description.js +98 -0
  47. package/dist/internal/app-release-artifact-reader.d.ts +26 -0
  48. package/dist/internal/app-release-artifact-reader.js +1 -0
  49. package/dist/internal/app-release-contract.d.ts +540 -0
  50. package/dist/internal/app-release-contract.js +169 -0
  51. package/dist/internal/app-release-schema.d.ts +1676 -0
  52. package/dist/internal/app-release-schema.js +386 -0
  53. package/dist/internal/app-release-store.d.ts +47 -0
  54. package/dist/internal/app-release-store.js +1 -0
  55. package/dist/internal/background-sync.d.ts +7 -0
  56. package/dist/internal/background-sync.js +10 -0
  57. package/dist/internal/deterministic-memory-app-release-store.d.ts +14 -0
  58. package/dist/internal/deterministic-memory-app-release-store.js +319 -0
  59. package/dist/internal/failure.d.ts +2 -0
  60. package/dist/internal/failure.js +9 -0
  61. package/dist/internal/immutable-release-material.d.ts +3 -0
  62. package/dist/internal/immutable-release-material.js +13 -0
  63. package/dist/internal/promise-app-release-store-adapter.d.ts +9 -0
  64. package/dist/internal/promise-app-release-store-adapter.js +65 -0
  65. package/dist/internal/promise-artifact-reader-adapter.d.ts +9 -0
  66. package/dist/internal/promise-artifact-reader-adapter.js +18 -0
  67. package/dist/internal/public-route-sql.d.ts +10 -0
  68. package/dist/internal/public-route-sql.js +239 -0
  69. package/dist/internal/public-website-authority.d.ts +338 -0
  70. package/dist/internal/public-website-authority.js +50 -0
  71. package/dist/internal/public-website-delivery.d.ts +59 -0
  72. package/dist/internal/public-website-delivery.js +232 -0
  73. package/dist/internal/public-website-fetch.d.ts +8 -0
  74. package/dist/internal/public-website-fetch.js +54 -0
  75. package/dist/internal/release-transition-material.d.ts +3 -0
  76. package/dist/internal/release-transition-material.js +11 -0
  77. package/dist/internal/verified-artifact-snapshot.d.ts +41 -0
  78. package/dist/internal/verified-artifact-snapshot.js +91 -0
  79. package/dist/public-route-moderation.d.ts +83 -0
  80. package/dist/public-route-moderation.js +236 -0
  81. package/dist/public-route-policy.d.ts +51 -0
  82. package/dist/public-route-policy.js +88 -0
  83. package/dist/public-routes.d.ts +34 -0
  84. package/dist/public-routes.js +136 -0
  85. package/dist/publisher.d.ts +61 -0
  86. package/dist/publisher.js +16 -0
  87. package/dist/safety-takedown.d.ts +56 -0
  88. package/dist/safety-takedown.js +146 -0
  89. package/dist/seller-closure-notice.d.ts +14 -0
  90. package/dist/seller-closure-notice.js +14 -0
  91. package/dist/seller-closure.d.ts +15 -0
  92. package/dist/seller-closure.js +31 -0
  93. package/dist/testing.d.ts +8 -0
  94. package/dist/testing.js +47 -0
  95. package/docs/api/README.md +26 -0
  96. package/docs/api/app-backend-binding-vocabulary.md +276 -0
  97. package/docs/api/app-backend-bindings.md +141 -0
  98. package/docs/api/app-event-facts.md +110 -0
  99. package/docs/api/app-method-context.md +166 -0
  100. package/docs/api/app-method-runtime.md +330 -0
  101. package/docs/api/backend.md +108 -0
  102. package/docs/api/background-replay.md +516 -0
  103. package/docs/api/cloudflare.md +1044 -0
  104. package/docs/api/effect.md +2429 -0
  105. package/docs/api/git-representation.md +56 -0
  106. package/docs/api/inbox.md +289 -0
  107. package/docs/api/index.md +2868 -0
  108. package/docs/api/publisher.md +147 -0
  109. package/docs/api/testing.md +31 -0
  110. package/package.json +158 -3
package/dist/app.js ADDED
@@ -0,0 +1,71 @@
1
+ export { appMethodCallContextSchema, } from "./app-method-context.js";
2
+ export { appBackendBindingKinds, appBackendBindingOperation, scopeOperationToMethod, appBackendBindingsSchema, appBackendShardBindingsSchema, } from "./app-backend-bindings.js";
3
+ import * as Schema from "effect/Schema";
4
+ export { appApiSchema, appApiReservedMethodNames, appApiMethodNameSchema, } from "./app-api.js";
5
+ import { appDeclarationSchema } from "./app-declaration.js";
6
+ import { isEmptyAppDeclaration, } from "./app-vocabulary.js";
7
+ import { artifactRefSchema, sha256DigestSchema, } from "./internal/app-release-schema.js";
8
+ /** One immutable Release root binds every surface published by the App. */
9
+ export const appMediaType = "application/vnd.fungi.app+json";
10
+ const objectReference = {
11
+ artifactRef: artifactRefSchema,
12
+ digest: sha256DigestSchema,
13
+ version: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
14
+ byteLength: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.makeFilter(Number.isSafeInteger))),
15
+ };
16
+ const appUiBuildSchema = Schema.Struct({
17
+ ...objectReference,
18
+ mediaType: Schema.Literals([
19
+ "text/html; charset=utf-8",
20
+ "application/vnd.fungi.app-assets+json",
21
+ ]),
22
+ });
23
+ const appBackendBuildSchema = Schema.Struct({
24
+ ...objectReference,
25
+ mediaType: Schema.Literal("application/vnd.fungi.app-backend+json"),
26
+ });
27
+ /** Omitted when the App requests nothing, so older roots stay valid. */
28
+ const declaration = {
29
+ declaration: Schema.optionalKey(appDeclarationSchema),
30
+ };
31
+ /**
32
+ * Closed child formats prevent recursive manifests and independent selection.
33
+ * An App may be visual, headless, or both, but never empty.
34
+ */
35
+ export const appBuildSchema = Schema.Union([
36
+ Schema.Struct({
37
+ format: Schema.Literal("fungi-app-v1"),
38
+ ui: appUiBuildSchema,
39
+ backend: appBackendBuildSchema,
40
+ ...declaration,
41
+ }),
42
+ Schema.Struct({
43
+ format: Schema.Literal("fungi-app-v1"),
44
+ ui: appUiBuildSchema,
45
+ ...declaration,
46
+ }),
47
+ Schema.Struct({
48
+ format: Schema.Literal("fungi-app-v1"),
49
+ backend: appBackendBuildSchema,
50
+ ...declaration,
51
+ }),
52
+ ]).pipe(Schema.check(Schema.makeFilter((app) => app.declaration?.api === undefined || "backend" in app, { message: "An App API requires a backend" })));
53
+ /** Maintained codec shared by artifact producers and consumers. */
54
+ export const appBuildJsonSchema = Schema.fromJsonString(appBuildSchema);
55
+ /** Construct one non-empty App root without leaking union mechanics to adapters. */
56
+ export function createAppBuild(ui, backend, appDeclaration) {
57
+ const declared = appDeclaration === undefined || isEmptyAppDeclaration(appDeclaration)
58
+ ? {}
59
+ : { declaration: appDeclaration };
60
+ if (ui === null) {
61
+ if (backend === null)
62
+ throw new Error("App build has no UI or backend");
63
+ return { format: "fungi-app-v1", backend, ...declared };
64
+ }
65
+ return backend === null
66
+ ? { format: "fungi-app-v1", ui, ...declared }
67
+ : { format: "fungi-app-v1", ui, backend, ...declared };
68
+ }
69
+ export { appCapabilityIdSchema, appDeclarationSchema, appDeclaredCommandSchema, appHandlerSchema, appRoleSchema, appStorageKeySchema, appStorageKeyNameSchema, } from "./app-declaration.js";
70
+ export { appCapabilityIds, appHandlers, appRoles, appStorageScopes, emptyAppDeclaration, isEmptyAppDeclaration, resolveAppRoles, } from "./app-vocabulary.js";
71
+ export { appCallsSchema } from "./app-declaration.js";
@@ -0,0 +1,115 @@
1
+ import * as Schema from "effect/Schema";
2
+ import type { AppReleaseD1Database } from "./cloudflare.js";
3
+ declare const actionSchema: Schema.Struct<{
4
+ readonly dispositionId: Schema.String;
5
+ readonly requestId: Schema.String;
6
+ readonly operatorId: Schema.String;
7
+ readonly reason: Schema.String;
8
+ readonly action: Schema.Union<readonly [Schema.Struct<{
9
+ readonly kind: Schema.Literal<"retain-detached">;
10
+ }>, Schema.Struct<{
11
+ readonly kind: Schema.Literal<"revoke-with-notice">;
12
+ }>]>;
13
+ }>;
14
+ export type ArtifactDispositionAction = Schema.Schema.Type<typeof actionSchema>;
15
+ /** The disposition owner supplies the target and reason; Hub adds no caller-asserted recipients. */
16
+ export declare const artifactDispositionNoticeSchema: Schema.Struct<{
17
+ readonly dispositionId: Schema.String;
18
+ readonly appId: Schema.refine<import("./index.js").AppId, Schema.String>;
19
+ readonly displayName: Schema.String;
20
+ readonly releaseIds: Schema.$Array<Schema.refine<import("./index.js").ReleaseId, Schema.String>>;
21
+ readonly reason: Schema.String;
22
+ }>;
23
+ export type ArtifactDispositionNotice = Schema.Schema.Type<typeof artifactDispositionNoticeSchema>;
24
+ export declare const artifactDispositionReadbackSchema: Schema.Struct<{
25
+ readonly releaseIds: Schema.$Array<Schema.refine<import("./index.js").ReleaseId, Schema.String>>;
26
+ readonly disposition_id: Schema.String;
27
+ readonly app_id: Schema.refine<import("./index.js").AppId, Schema.String>;
28
+ readonly release_ids_json: Schema.String;
29
+ readonly artifact_roots_json: Schema.String;
30
+ readonly created_at: Schema.Number;
31
+ readonly state: Schema.Literals<readonly ["pending", "retaining", "retained", "revoking", "revoked"]>;
32
+ readonly request_id: Schema.NullOr<Schema.String>;
33
+ readonly reason: Schema.NullOr<Schema.String>;
34
+ readonly completed_at: Schema.NullOr<Schema.Number>;
35
+ }>;
36
+ /** Buyer artifact custody is independent of erased seller identity. Operators bind disposition to one request; captured buyer Teams require durable inbox receipts. */
37
+ export declare function createD1ArtifactDispositions(database: AppReleaseD1Database, now?: () => number): {
38
+ read: (dispositionId: string) => Promise<{
39
+ releaseIds: readonly import("./index.js").ReleaseId[];
40
+ disposition_id: string;
41
+ app_id: import("./index.js").AppId;
42
+ release_ids_json: string;
43
+ artifact_roots_json: string;
44
+ created_at: number;
45
+ state: "pending" | "revoked" | "retaining" | "retained" | "revoking";
46
+ request_id: string | null;
47
+ reason: string | null;
48
+ completed_at: number | null;
49
+ } | null>;
50
+ prepare: (raw: ArtifactDispositionAction) => Promise<{
51
+ kind: "conflict";
52
+ disposition?: never;
53
+ } | {
54
+ kind: "recorded";
55
+ disposition: {
56
+ releaseIds: readonly import("./index.js").ReleaseId[];
57
+ disposition_id: string;
58
+ app_id: import("./index.js").AppId;
59
+ release_ids_json: string;
60
+ artifact_roots_json: string;
61
+ created_at: number;
62
+ state: "pending" | "revoked" | "retaining" | "retained" | "revoking";
63
+ request_id: string | null;
64
+ reason: string | null;
65
+ completed_at: number | null;
66
+ };
67
+ } | {
68
+ kind: "unavailable";
69
+ disposition?: never;
70
+ }>;
71
+ complete: (dispositionId: string, operatorId: string) => Promise<{
72
+ kind: "recorded";
73
+ disposition: {
74
+ releaseIds: readonly import("./index.js").ReleaseId[];
75
+ disposition_id: string;
76
+ app_id: import("./index.js").AppId;
77
+ release_ids_json: string;
78
+ artifact_roots_json: string;
79
+ created_at: number;
80
+ state: "pending" | "revoked" | "retaining" | "retained" | "revoking";
81
+ request_id: string | null;
82
+ reason: string | null;
83
+ completed_at: number | null;
84
+ };
85
+ } | {
86
+ kind: "conflict";
87
+ disposition?: never;
88
+ }>;
89
+ pending: (after?: string) => Promise<({
90
+ releaseIds: readonly import("./index.js").ReleaseId[];
91
+ disposition_id: string;
92
+ app_id: import("./index.js").AppId;
93
+ release_ids_json: string;
94
+ artifact_roots_json: string;
95
+ created_at: number;
96
+ state: "pending" | "revoked" | "retaining" | "retained" | "revoking";
97
+ request_id: string | null;
98
+ reason: string | null;
99
+ completed_at: number | null;
100
+ } | null)[]>;
101
+ noticeRecipients: (dispositionId: string, after?: string) => Promise<{
102
+ readonly teamId: string | null;
103
+ readonly receipt: string | null;
104
+ readonly erasedBy: string | null;
105
+ readonly erasedAt: number | null;
106
+ }[]>;
107
+ recordNoticeReceipt: (dispositionId: string, teamId: string, receipt: string) => Promise<boolean>;
108
+ recordNoticeRecipientErased: (dispositionId: string, teamId: string, operatorId: string) => Promise<boolean>;
109
+ pendingWork: (dispositionId: string) => Promise<{
110
+ teamIds: string[];
111
+ releaseIds: import("./index.js").ReleaseId[];
112
+ }>;
113
+ };
114
+ export declare const artifactDispositionSchemaSql: string;
115
+ export {};
@@ -0,0 +1,234 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { appIdSchema, artifactRefSchema, releaseIdSchema, } 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(256)));
5
+ const reason = Schema.String.pipe(Schema.check(Schema.isPattern(/\S/u)), Schema.check(Schema.isMaxLength(1000)));
6
+ const actionSchema = Schema.Struct({
7
+ dispositionId: identity,
8
+ requestId: identity,
9
+ operatorId: identity,
10
+ reason,
11
+ action: Schema.Union([
12
+ Schema.Struct({ kind: Schema.Literal("retain-detached") }),
13
+ Schema.Struct({ kind: Schema.Literal("revoke-with-notice") }),
14
+ ]),
15
+ });
16
+ /** The disposition owner supplies the target and reason; Hub adds no caller-asserted recipients. */
17
+ export const artifactDispositionNoticeSchema = Schema.Struct({
18
+ dispositionId: identity,
19
+ appId: appIdSchema,
20
+ displayName: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(64))),
21
+ releaseIds: Schema.Array(releaseIdSchema).pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(10000))),
22
+ reason,
23
+ });
24
+ const rowSchema = Schema.Struct({
25
+ disposition_id: identity,
26
+ app_id: appIdSchema,
27
+ release_ids_json: Schema.String,
28
+ artifact_roots_json: Schema.String,
29
+ created_at: Schema.Number,
30
+ state: Schema.Literals([
31
+ "pending",
32
+ "retaining",
33
+ "retained",
34
+ "revoking",
35
+ "revoked",
36
+ ]),
37
+ request_id: Schema.NullOr(identity),
38
+ reason: Schema.NullOr(reason),
39
+ completed_at: Schema.NullOr(Schema.Number),
40
+ });
41
+ export const artifactDispositionReadbackSchema = Schema.Struct({
42
+ ...rowSchema.fields,
43
+ releaseIds: Schema.Array(releaseIdSchema).pipe(Schema.check(Schema.isMaxLength(10000))),
44
+ });
45
+ const releasesSchema = Schema.fromJsonString(Schema.Array(releaseIdSchema).pipe(Schema.check(Schema.isMaxLength(10000))));
46
+ const artifactsSchema = Schema.fromJsonString(Schema.Array(artifactRefSchema).pipe(Schema.check(Schema.isMaxLength(10000))));
47
+ /** Buyer artifact custody is independent of erased seller identity. Operators bind disposition to one request; captured buyer Teams require durable inbox receipts. */
48
+ export function createD1ArtifactDispositions(database, now = Date.now) {
49
+ const read = async (dispositionId) => {
50
+ const row = await database
51
+ .prepare("SELECT * FROM app_release_artifact_dispositions WHERE disposition_id=?")
52
+ .bind(dispositionId)
53
+ .first();
54
+ if (row === null)
55
+ return null;
56
+ const decoded = Schema.decodeUnknownSync(rowSchema)(row);
57
+ Schema.decodeUnknownSync(artifactsSchema)(decoded.artifact_roots_json);
58
+ return {
59
+ ...decoded,
60
+ releaseIds: Schema.decodeUnknownSync(releasesSchema)(decoded.release_ids_json),
61
+ };
62
+ };
63
+ const prepare = async (raw) => {
64
+ try {
65
+ const input = Schema.decodeUnknownSync(actionSchema, {
66
+ onExcessProperty: "error",
67
+ })(raw);
68
+ const state = input.action.kind === "retain-detached" ? "retaining" : "revoking";
69
+ const results = await database.batch([
70
+ database
71
+ .prepare(`UPDATE app_release_artifact_dispositions SET state=?,request_id=?,reason=?
72
+ WHERE disposition_id=? AND state='pending'`)
73
+ .bind(state, input.requestId, input.reason, input.dispositionId),
74
+ database
75
+ .prepare(`INSERT INTO app_release_artifact_disposition_steps(disposition_id,phase,operator_id,recorded_at)
76
+ SELECT ?,'prepared',?,? WHERE EXISTS (SELECT 1 FROM app_release_artifact_dispositions
77
+ WHERE disposition_id=? AND state IN (?,?) AND request_id=? AND reason=?)
78
+ ON CONFLICT DO NOTHING`)
79
+ .bind(input.dispositionId, input.operatorId, now(), input.dispositionId, state, state === "retaining" ? "retained" : "revoked", input.requestId, input.reason),
80
+ ]);
81
+ if (results.some((result) => !result.success))
82
+ throw new Error("Artifact disposition unavailable");
83
+ const saved = await read(input.dispositionId);
84
+ if (saved?.request_id !== input.requestId ||
85
+ saved.reason !== input.reason ||
86
+ ![state, state === "retaining" ? "retained" : "revoked"].includes(saved.state))
87
+ return { kind: "conflict" };
88
+ return { kind: "recorded", disposition: saved };
89
+ }
90
+ catch (cause) {
91
+ reportFailure("artifact disposition preparation", cause);
92
+ return { kind: "unavailable" };
93
+ }
94
+ };
95
+ const complete = async (dispositionId, operatorId) => {
96
+ for (const value of [dispositionId, operatorId])
97
+ Schema.decodeUnknownSync(identity)(value);
98
+ const results = await database.batch([
99
+ database
100
+ .prepare(`INSERT INTO app_release_artifact_disposition_steps(disposition_id,phase,operator_id,recorded_at)
101
+ SELECT ?,'completed',?,? WHERE EXISTS (SELECT 1 FROM app_release_artifact_dispositions d WHERE d.disposition_id=? AND
102
+ (d.state IN ('retaining','retained','revoked') OR (d.state='revoking' AND NOT EXISTS (
103
+ SELECT 1 FROM app_release_artifact_notice_recipients n WHERE n.disposition_id=d.disposition_id AND n.receipt IS NULL AND n.erased_at IS NULL) AND NOT EXISTS (
104
+ SELECT 1 FROM json_each(d.release_ids_json) r WHERE NOT EXISTS (
105
+ SELECT 1 FROM app_release_transitions t JOIN app_release_transition_obligations o ON o.sequence=t.sequence
106
+ WHERE t.release_id=r.value AND t.operation_kind='revocation' AND o.state='acknowledged')))))
107
+ ON CONFLICT DO NOTHING`)
108
+ .bind(dispositionId, operatorId, now(), dispositionId),
109
+ database
110
+ .prepare(`UPDATE app_release_artifact_dispositions SET state=CASE state WHEN 'retaining' THEN 'retained' ELSE 'revoked' END,completed_at=?
111
+ WHERE disposition_id=? AND state IN ('retaining','revoking') AND EXISTS (
112
+ SELECT 1 FROM app_release_artifact_disposition_steps WHERE disposition_id=? AND phase='completed')`)
113
+ .bind(now(), dispositionId, dispositionId),
114
+ ]);
115
+ if (results.some((result) => !result.success))
116
+ throw new Error("Artifact disposition unavailable");
117
+ const saved = await read(dispositionId);
118
+ return saved !== null &&
119
+ (saved.state === "retained" || saved.state === "revoked")
120
+ ? { kind: "recorded", disposition: saved }
121
+ : { kind: "conflict" };
122
+ };
123
+ const pending = async (after = "") => {
124
+ const rows = await database
125
+ .prepare(`SELECT disposition_id FROM app_release_artifact_dispositions
126
+ WHERE state IN ('pending','retaining','revoking') AND disposition_id>? ORDER BY disposition_id LIMIT 100`)
127
+ .bind(after)
128
+ .all();
129
+ const ids = rows.results.map((row) => Schema.decodeUnknownSync(Schema.Struct({ disposition_id: identity }))(row).disposition_id);
130
+ return Promise.all(ids.map(read));
131
+ };
132
+ const noticeRow = Schema.Struct({
133
+ teamId: Schema.NullOr(identity),
134
+ receipt: Schema.NullOr(identity),
135
+ erasedBy: Schema.NullOr(identity),
136
+ erasedAt: Schema.NullOr(Schema.Number),
137
+ });
138
+ const noticeRecipients = async (dispositionId, after = "") => {
139
+ const rows = await database
140
+ .prepare(`SELECT buyer_team_id AS teamId,receipt,erased_by AS erasedBy,erased_at AS erasedAt FROM app_release_artifact_notice_recipients
141
+ WHERE disposition_id=? AND (buyer_team_id>? OR buyer_team_id IS NULL) ORDER BY buyer_team_id,recipient_id LIMIT 100`)
142
+ .bind(dispositionId, after)
143
+ .all();
144
+ return rows.results.map((row) => Schema.decodeUnknownSync(noticeRow)(row));
145
+ };
146
+ /** Only the trusted Hub delivery coordinator calls this after the Team inbox owner commits its receipt. */
147
+ const recordNoticeReceipt = async (dispositionId, teamId, receipt) => {
148
+ for (const value of [dispositionId, teamId, receipt])
149
+ Schema.decodeUnknownSync(identity)(value);
150
+ await database
151
+ .prepare(`UPDATE app_release_artifact_notice_recipients SET receipt=?
152
+ WHERE disposition_id=? AND buyer_team_id=? AND receipt IS NULL AND erased_at IS NULL AND EXISTS (
153
+ SELECT 1 FROM app_release_artifact_dispositions d WHERE d.disposition_id=? AND d.state='revoking')`)
154
+ .bind(receipt, dispositionId, teamId, dispositionId)
155
+ .run();
156
+ const row = await database
157
+ .prepare(`SELECT receipt FROM app_release_artifact_notice_recipients
158
+ WHERE disposition_id=? AND buyer_team_id=?`)
159
+ .bind(dispositionId, teamId)
160
+ .first();
161
+ return (row !== null &&
162
+ Schema.decodeUnknownSync(Schema.Struct({ receipt: Schema.NullOr(identity) }))(row).receipt === receipt);
163
+ };
164
+ /** Trusted Hub coordinator verifies the Team owner's erasure readback before calling this. Never a delivered receipt. */
165
+ const recordNoticeRecipientErased = async (dispositionId, teamId, operatorId) => {
166
+ for (const value of [dispositionId, teamId, operatorId])
167
+ Schema.decodeUnknownSync(identity)(value);
168
+ await database
169
+ .prepare(`UPDATE app_release_artifact_notice_recipients SET erased_by=?,erased_at=?
170
+ WHERE disposition_id=? AND buyer_team_id=? AND receipt IS NULL AND erased_at IS NULL AND EXISTS (
171
+ SELECT 1 FROM app_release_artifact_dispositions d WHERE d.disposition_id=? AND d.state='revoking')`)
172
+ .bind(operatorId, now(), dispositionId, teamId, dispositionId)
173
+ .run();
174
+ const row = await database
175
+ .prepare(`SELECT erased_at FROM app_release_artifact_notice_recipients
176
+ WHERE disposition_id=? AND buyer_team_id=?`)
177
+ .bind(dispositionId, teamId)
178
+ .first();
179
+ return (row !== null &&
180
+ Schema.decodeUnknownSync(Schema.Struct({ erased_at: Schema.NullOr(Schema.Number) }))(row).erased_at !== null);
181
+ };
182
+ const pendingWork = async (dispositionId) => {
183
+ const notices = await database
184
+ .prepare(`SELECT buyer_team_id AS teamId FROM app_release_artifact_notice_recipients
185
+ WHERE disposition_id=? AND receipt IS NULL AND erased_at IS NULL ORDER BY buyer_team_id LIMIT 100`)
186
+ .bind(dispositionId)
187
+ .all();
188
+ const releases = await database
189
+ .prepare(`SELECT r.value AS releaseId FROM app_release_artifact_dispositions d,json_each(d.release_ids_json) r
190
+ WHERE d.disposition_id=? AND d.state='revoking' AND NOT EXISTS (
191
+ SELECT 1 FROM app_release_transitions t JOIN app_release_transition_obligations o ON o.sequence=t.sequence
192
+ WHERE t.release_id=r.value AND t.operation_kind='revocation' AND o.state='acknowledged')
193
+ ORDER BY r.value LIMIT 100`)
194
+ .bind(dispositionId)
195
+ .all();
196
+ return {
197
+ teamIds: notices.results.map((row) => Schema.decodeUnknownSync(Schema.Struct({ teamId: identity }))(row)
198
+ .teamId),
199
+ releaseIds: releases.results.map((row) => Schema.decodeUnknownSync(Schema.Struct({ releaseId: releaseIdSchema }))(row).releaseId),
200
+ };
201
+ };
202
+ return {
203
+ read,
204
+ prepare,
205
+ complete,
206
+ pending,
207
+ noticeRecipients,
208
+ recordNoticeReceipt,
209
+ recordNoticeRecipientErased,
210
+ pendingWork,
211
+ };
212
+ }
213
+ export const artifactDispositionSchemaSql = [
214
+ `CREATE TABLE app_release_artifact_dispositions(disposition_id TEXT PRIMARY KEY, app_id TEXT NOT NULL UNIQUE REFERENCES app_release_apps(app_id),
215
+ release_ids_json TEXT NOT NULL CHECK(json_valid(release_ids_json)), artifact_roots_json TEXT NOT NULL CHECK(json_valid(artifact_roots_json)),
216
+ created_at INTEGER NOT NULL,state TEXT NOT NULL CHECK(state IN ('pending','retaining','retained','revoking','revoked')),
217
+ request_id TEXT,reason TEXT,completed_at INTEGER,
218
+ CHECK((state='pending' AND request_id IS NULL AND reason IS NULL) OR (state<>'pending' AND request_id IS NOT NULL AND length(request_id) BETWEEN 1 AND 256 AND reason IS NOT NULL AND length(trim(reason)) BETWEEN 1 AND 1000)),
219
+ CHECK((state IN ('retained','revoked'))=(completed_at IS NOT NULL))) STRICT;`,
220
+ `CREATE TABLE app_release_artifact_notice_recipients(recipient_id INTEGER PRIMARY KEY,disposition_id TEXT NOT NULL REFERENCES app_release_artifact_dispositions(disposition_id),
221
+ buyer_team_id TEXT CHECK(buyer_team_id IS NULL OR length(buyer_team_id) BETWEEN 1 AND 256),receipt TEXT CHECK(receipt IS NULL OR length(receipt) BETWEEN 1 AND 256),
222
+ erased_by TEXT,erased_at INTEGER,
223
+ CHECK((erased_by IS NULL)=(erased_at IS NULL)),
224
+ CHECK(erased_by IS NULL OR length(erased_by) BETWEEN 1 AND 256),
225
+ CHECK(receipt IS NULL OR erased_at IS NULL),
226
+ CHECK(buyer_team_id IS NOT NULL OR erased_at IS NOT NULL),
227
+ UNIQUE(disposition_id,buyer_team_id)) STRICT;`,
228
+ `CREATE INDEX app_release_artifact_dispositions_pending ON app_release_artifact_dispositions(state,disposition_id);`,
229
+ `CREATE TABLE app_release_artifact_disposition_steps(step_id INTEGER PRIMARY KEY,disposition_id TEXT NOT NULL REFERENCES app_release_artifact_dispositions(disposition_id),
230
+ phase TEXT NOT NULL CHECK(phase IN ('prepared','completed')),operator_id TEXT NOT NULL,recorded_at INTEGER NOT NULL) STRICT;`,
231
+ `CREATE INDEX app_release_artifact_disposition_steps_by_action ON app_release_artifact_disposition_steps(disposition_id,phase);`,
232
+ ]
233
+ .map((statement) => statement.replaceAll("\n", " "))
234
+ .join("\n");
@@ -0,0 +1,54 @@
1
+ import * as Schema from "effect/Schema";
2
+ import type { ReleaseId, StableRoute } from "./internal/app-release-contract.js";
3
+ /** Explicit root representation; existing HTML and backend artifacts remain distinct. */
4
+ export declare const assetBuildMediaType: "application/vnd.fungi.app-assets+json";
5
+ /** Trusted host policy, independently bounded from single-document HTML. */
6
+ export declare const assetBuildPolicySchema: Schema.Struct<{
7
+ readonly maxManifestBytes: Schema.Number;
8
+ readonly maxObjects: Schema.Number;
9
+ readonly maxObjectBytes: Schema.Number;
10
+ readonly maxTotalBytes: Schema.Number;
11
+ }>;
12
+ export type AssetBuildPolicy = Schema.Schema.Type<typeof assetBuildPolicySchema>;
13
+ /** Captured Vite base: a canonical pathname, or a relative private host mount. */
14
+ export declare const assetBaseSchema: Schema.String;
15
+ /** StableRoute already owns the canonical single-slug grammar. */
16
+ export declare function publicAssetBase(stableRoute: StableRoute, releaseId: ReleaseId): string;
17
+ /** Vite output names are relative POSIX paths, never request URLs or storage keys. */
18
+ export declare const assetPathSchema: Schema.String;
19
+ export declare const assetBuildSchema: Schema.Struct<{
20
+ readonly format: Schema.Literal<"fungi-assets-v1">;
21
+ readonly base: Schema.String;
22
+ readonly assets: Schema.$Array<Schema.Struct<{
23
+ readonly path: Schema.String;
24
+ readonly artifactRef: Schema.refine<import("./internal/app-release-contract.js").ArtifactRef, Schema.String>;
25
+ readonly digest: Schema.refine<import("./internal/app-release-contract.js").Sha256Digest, Schema.String>;
26
+ readonly version: Schema.String;
27
+ readonly byteLength: Schema.Number;
28
+ readonly contentType: Schema.String;
29
+ }>>;
30
+ }>;
31
+ export declare const assetBuildJsonSchema: Schema.fromJsonString<Schema.Struct<{
32
+ readonly format: Schema.Literal<"fungi-assets-v1">;
33
+ readonly base: Schema.String;
34
+ readonly assets: Schema.$Array<Schema.Struct<{
35
+ readonly path: Schema.String;
36
+ readonly artifactRef: Schema.refine<import("./internal/app-release-contract.js").ArtifactRef, Schema.String>;
37
+ readonly digest: Schema.refine<import("./internal/app-release-contract.js").Sha256Digest, Schema.String>;
38
+ readonly version: Schema.String;
39
+ readonly byteLength: Schema.Number;
40
+ readonly contentType: Schema.String;
41
+ }>>;
42
+ }>>;
43
+ export type AssetBuild = Schema.Schema.Type<typeof assetBuildSchema>;
44
+ export type BuildAsset = AssetBuild["assets"][number];
45
+ /**
46
+ * A shell App's module: an ES module exporting `mountShell`, shipped beside
47
+ * `index.html`. A host composes the shell its Team picked by importing it
48
+ * from the host's own origin into the top-level page.
49
+ */
50
+ export declare const shellModuleAssetPath = "/shell.js";
51
+ /** The shell module this asset build ships, if it ships one. */
52
+ export declare function shellModuleAsset(build: AssetBuild): BuildAsset | undefined;
53
+ /** Decode the digest-covered root and enforce bounds before touching its objects. */
54
+ export declare function decodeAssetBuild(bytes: Uint8Array, policy: AssetBuildPolicy): AssetBuild;
package/dist/assets.js ADDED
@@ -0,0 +1,89 @@
1
+ import { appOriginPaths } from "./app-origin-paths.js";
2
+ import * as Schema from "effect/Schema";
3
+ import { artifactRefSchema, sha256DigestSchema, } from "./internal/app-release-schema.js";
4
+ /** Explicit root representation; existing HTML and backend artifacts remain distinct. */
5
+ export const assetBuildMediaType = "application/vnd.fungi.app-assets+json";
6
+ const positiveInteger = Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.makeFilter(Number.isSafeInteger)));
7
+ /** Trusted host policy, independently bounded from single-document HTML. */
8
+ export const assetBuildPolicySchema = Schema.Struct({
9
+ maxManifestBytes: positiveInteger,
10
+ maxObjects: positiveInteger,
11
+ maxObjectBytes: positiveInteger,
12
+ maxTotalBytes: positiveInteger,
13
+ });
14
+ /** Captured Vite base: a canonical pathname, or a relative private host mount. */
15
+ export const assetBaseSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => {
16
+ if (value == "./")
17
+ return true;
18
+ const origin = "https://assets.invalid";
19
+ if (!URL.canParse(value, origin))
20
+ return false;
21
+ const url = new URL(value, origin);
22
+ return (value.startsWith("/") &&
23
+ value.endsWith("/") &&
24
+ url.origin === origin &&
25
+ url.pathname === value &&
26
+ url.search === "" &&
27
+ url.hash === "");
28
+ })));
29
+ /** StableRoute already owns the canonical single-slug grammar. */
30
+ export function publicAssetBase(stableRoute, releaseId) {
31
+ return `${appOriginPaths.surfaceAssets}/${stableRoute.slice(1)}/releases/${encodeURIComponent(releaseId)}/`;
32
+ }
33
+ /** Vite output names are relative POSIX paths, never request URLs or storage keys. */
34
+ export const assetPathSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((path) => path.startsWith("/") &&
35
+ !path.includes("\\") &&
36
+ !/\p{Cc}/u.test(path) &&
37
+ path
38
+ .slice(1)
39
+ .split("/")
40
+ .every((part) => part !== "" && part !== "." && part !== ".."))));
41
+ const assetSchema = Schema.Struct({
42
+ path: assetPathSchema,
43
+ artifactRef: artifactRefSchema,
44
+ digest: sha256DigestSchema,
45
+ version: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
46
+ byteLength: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.makeFilter(Number.isSafeInteger))),
47
+ contentType: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isPattern(/^[^\r\n]+$/u))),
48
+ });
49
+ export const assetBuildSchema = Schema.Struct({
50
+ format: Schema.Literal("fungi-assets-v1"),
51
+ base: assetBaseSchema,
52
+ assets: Schema.Array(assetSchema),
53
+ });
54
+ export const assetBuildJsonSchema = Schema.fromJsonString(assetBuildSchema);
55
+ /**
56
+ * A shell App's module: an ES module exporting `mountShell`, shipped beside
57
+ * `index.html`. A host composes the shell its Team picked by importing it
58
+ * from the host's own origin into the top-level page.
59
+ */
60
+ export const shellModuleAssetPath = "/shell.js";
61
+ /** The shell module this asset build ships, if it ships one. */
62
+ export function shellModuleAsset(build) {
63
+ return build.assets.find((asset) => asset.path === shellModuleAssetPath);
64
+ }
65
+ /** Decode the digest-covered root and enforce bounds before touching its objects. */
66
+ export function decodeAssetBuild(bytes, policy) {
67
+ const limits = Schema.decodeUnknownSync(assetBuildPolicySchema)(policy);
68
+ if (bytes.byteLength > limits.maxManifestBytes)
69
+ throw new Error("Asset manifest exceeds policy");
70
+ const build = Schema.decodeUnknownSync(assetBuildJsonSchema)(new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), { onExcessProperty: "error" });
71
+ if (build.assets.length === 0 || build.assets.length > limits.maxObjects)
72
+ throw new Error("Asset count exceeds policy");
73
+ const paths = new Set();
74
+ let total = 0;
75
+ for (const asset of build.assets) {
76
+ if (paths.has(asset.path))
77
+ throw new Error("Duplicate asset path");
78
+ paths.add(asset.path);
79
+ total += asset.byteLength;
80
+ if (asset.byteLength > limits.maxObjectBytes ||
81
+ !Number.isSafeInteger(total) ||
82
+ total > limits.maxTotalBytes)
83
+ throw new Error("Asset bytes exceed policy");
84
+ }
85
+ const index = build.assets.find((asset) => asset.path === "/index.html");
86
+ if (!index?.contentType.startsWith("text/html"))
87
+ throw new Error("Asset build requires HTML index");
88
+ return build;
89
+ }
@@ -0,0 +1,41 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import type { AppReleaseCore } from "./effect.js";
4
+ import type { ReleaseId } from "./index.js";
5
+ /** This immutable descriptor is stored inside the verified Release artifact. */
6
+ export declare const durableBackendSchema: Schema.Struct<{
7
+ readonly runtime: Schema.Struct<{
8
+ readonly kind: Schema.Literal<"durable-facet">;
9
+ readonly stateSchema: Schema.Number;
10
+ }>;
11
+ readonly compatibilityDate: Schema.String;
12
+ readonly exportName: Schema.String;
13
+ readonly code: Schema.String;
14
+ }>;
15
+ export type { DurableBackend } from "./internal/app-release-contract.js";
16
+ /** Content type separating executable backend artifacts from website HTML. */
17
+ export declare const backendMediaType: "application/vnd.fungi.app-backend+json";
18
+ /** Maintained JSON codec for immutable backend bytes. */
19
+ export declare const backendJsonSchema: Schema.fromJsonString<Schema.Struct<{
20
+ readonly runtime: Schema.Struct<{
21
+ readonly kind: Schema.Literal<"durable-facet">;
22
+ readonly stateSchema: Schema.Number;
23
+ }>;
24
+ readonly compatibilityDate: Schema.String;
25
+ readonly exportName: Schema.String;
26
+ readonly code: Schema.String;
27
+ }>>;
28
+ /** Resolve through catalog eligibility and digest verification before decoding code. */
29
+ export declare function resolveDurableBackend(catalog: Pick<AppReleaseCore, "resolveEligibleApp">, releaseId: ReleaseId): Effect.Effect<{
30
+ release: import("./index.js").Release;
31
+ digest: import("./index.js").Sha256Digest;
32
+ backend: Readonly<{
33
+ readonly runtime: Readonly<{
34
+ readonly kind: "durable-facet";
35
+ readonly stateSchema: number;
36
+ }>;
37
+ readonly compatibilityDate: string;
38
+ readonly exportName: string;
39
+ readonly code: string;
40
+ }>;
41
+ }, import("./effect.js").AppReleaseCoreFailure, never>;
@@ -0,0 +1,29 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ /** This immutable descriptor is stored inside the verified Release artifact. */
4
+ export const durableBackendSchema = Schema.Struct({
5
+ runtime: Schema.Struct({
6
+ kind: Schema.Literal("durable-facet"),
7
+ stateSchema: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0))),
8
+ }),
9
+ compatibilityDate: Schema.String.pipe(Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/))),
10
+ exportName: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
11
+ code: Schema.String.pipe(Schema.check(Schema.isMinLength(1))),
12
+ });
13
+ /** Content type separating executable backend artifacts from website HTML. */
14
+ export const backendMediaType = "application/vnd.fungi.app-backend+json";
15
+ /** Maintained JSON codec for immutable backend bytes. */
16
+ export const backendJsonSchema = Schema.fromJsonString(durableBackendSchema);
17
+ /** Resolve through catalog eligibility and digest verification before decoding code. */
18
+ export function resolveDurableBackend(catalog, releaseId) {
19
+ return Effect.gen(function* () {
20
+ const app = yield* catalog.resolveEligibleApp(releaseId);
21
+ if (app.backend === null)
22
+ return yield* Effect.fail(new Error("Release has no durable backend"));
23
+ return {
24
+ release: app.release,
25
+ digest: app.backend.digest,
26
+ backend: app.backend.descriptor,
27
+ };
28
+ });
29
+ }