@nakafa/aksara-contracts 0.27.0
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 +185 -0
- package/README.md +73 -0
- package/dist/artifact/integrity.d.ts +29 -0
- package/dist/artifact/integrity.js +54 -0
- package/dist/artifact/limits.d.ts +26 -0
- package/dist/artifact/limits.js +46 -0
- package/dist/artifact/source.d.ts +21 -0
- package/dist/artifact/source.js +24 -0
- package/dist/artifact/spec.d.ts +148 -0
- package/dist/artifact/spec.js +92 -0
- package/dist/artifact/verify.d.ts +26 -0
- package/dist/artifact/verify.js +18 -0
- package/dist/cache/content.d.ts +52 -0
- package/dist/cache/content.js +80 -0
- package/dist/content.d.ts +168 -0
- package/dist/content.js +107 -0
- package/dist/country.d.ts +4 -0
- package/dist/country.js +8 -0
- package/dist/date.d.ts +24 -0
- package/dist/date.js +44 -0
- package/dist/decode.d.ts +4 -0
- package/dist/decode.js +8 -0
- package/dist/delivery.d.ts +4 -0
- package/dist/delivery.js +7 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +8 -0
- package/dist/graph/family.d.ts +45 -0
- package/dist/graph/family.js +85 -0
- package/dist/graph/identity.d.ts +19 -0
- package/dist/graph/identity.js +39 -0
- package/dist/graph/spec.d.ts +20 -0
- package/dist/graph/spec.js +26 -0
- package/dist/hash/text.d.ts +10 -0
- package/dist/hash/text.js +20 -0
- package/dist/ids.d.ts +27 -0
- package/dist/ids.js +63 -0
- package/dist/limits.d.ts +10 -0
- package/dist/limits.js +10 -0
- package/dist/locale.d.ts +37 -0
- package/dist/locale.js +53 -0
- package/dist/material/domain.d.ts +4 -0
- package/dist/material/domain.js +8 -0
- package/dist/preview/artifact.d.ts +198 -0
- package/dist/preview/artifact.js +20 -0
- package/dist/preview/auth.d.ts +71 -0
- package/dist/preview/auth.js +49 -0
- package/dist/preview/document.d.ts +692 -0
- package/dist/preview/document.js +90 -0
- package/dist/preview/spec.d.ts +2517 -0
- package/dist/preview/spec.js +142 -0
- package/dist/preview/target.d.ts +172 -0
- package/dist/preview/target.js +70 -0
- package/dist/program/curriculum.d.ts +69 -0
- package/dist/program/curriculum.js +181 -0
- package/dist/program/snapshot/digest.d.ts +26 -0
- package/dist/program/snapshot/digest.js +220 -0
- package/dist/program/snapshot/hash.d.ts +31 -0
- package/dist/program/snapshot/hash.js +40 -0
- package/dist/program/snapshot/row-hash.d.ts +155 -0
- package/dist/program/snapshot/row-hash.js +34 -0
- package/dist/program/snapshot/row.d.ts +139 -0
- package/dist/program/snapshot/row.js +28 -0
- package/dist/program/snapshot/spec.d.ts +36 -0
- package/dist/program/snapshot/spec.js +39 -0
- package/dist/program/spec.d.ts +85 -0
- package/dist/program/spec.js +197 -0
- package/dist/projection/article.d.ts +145 -0
- package/dist/projection/article.js +150 -0
- package/dist/projection/digest.d.ts +261 -0
- package/dist/projection/digest.js +59 -0
- package/dist/projection/hash.d.ts +3 -0
- package/dist/projection/hash.js +10 -0
- package/dist/projection/material.d.ts +109 -0
- package/dist/projection/material.js +160 -0
- package/dist/projection/page.d.ts +112 -0
- package/dist/projection/page.js +106 -0
- package/dist/projection/question.d.ts +520 -0
- package/dist/projection/question.js +109 -0
- package/dist/projection/spec.d.ts +503 -0
- package/dist/projection/spec.js +56 -0
- package/dist/projection/verify.d.ts +281 -0
- package/dist/projection/verify.js +86 -0
- package/dist/question/identity.d.ts +175 -0
- package/dist/question/identity.js +205 -0
- package/dist/question/item.d.ts +453 -0
- package/dist/question/item.js +146 -0
- package/dist/question/response.d.ts +175 -0
- package/dist/question/response.js +150 -0
- package/dist/quran/attribution.d.ts +48 -0
- package/dist/quran/attribution.js +44 -0
- package/dist/quran/identity.d.ts +105 -0
- package/dist/quran/identity.js +100 -0
- package/dist/quran/notes.d.ts +56 -0
- package/dist/quran/notes.js +142 -0
- package/dist/quran/provenance.d.ts +257 -0
- package/dist/quran/provenance.js +144 -0
- package/dist/quran/snapshot/digest.d.ts +33 -0
- package/dist/quran/snapshot/digest.js +232 -0
- package/dist/quran/snapshot/hash.d.ts +51 -0
- package/dist/quran/snapshot/hash.js +50 -0
- package/dist/quran/snapshot/row/hash.d.ts +748 -0
- package/dist/quran/snapshot/row/hash.js +115 -0
- package/dist/quran/snapshot/row.d.ts +354 -0
- package/dist/quran/snapshot/row.js +91 -0
- package/dist/quran/snapshot/spec.d.ts +50 -0
- package/dist/quran/snapshot/spec.js +66 -0
- package/dist/quran/source.d.ts +206 -0
- package/dist/quran/source.js +153 -0
- package/dist/quran/spec.d.ts +47 -0
- package/dist/quran/spec.js +41 -0
- package/dist/quran/text.d.ts +7 -0
- package/dist/quran/text.js +11 -0
- package/dist/release/canonical.d.ts +23 -0
- package/dist/release/canonical.js +29 -0
- package/dist/release/current/evidence.d.ts +424 -0
- package/dist/release/current/evidence.js +41 -0
- package/dist/release/current/runtime.d.ts +4 -0
- package/dist/release/current/runtime.js +15 -0
- package/dist/release/current/state.d.ts +567 -0
- package/dist/release/current/state.js +91 -0
- package/dist/release/digest.d.ts +54 -0
- package/dist/release/digest.js +71 -0
- package/dist/release/hash.d.ts +72 -0
- package/dist/release/hash.js +14 -0
- package/dist/release/head.d.ts +206 -0
- package/dist/release/head.js +131 -0
- package/dist/release/items.d.ts +98 -0
- package/dist/release/items.js +114 -0
- package/dist/release/lifecycle.d.ts +297 -0
- package/dist/release/lifecycle.js +97 -0
- package/dist/release/origin.d.ts +30 -0
- package/dist/release/origin.js +24 -0
- package/dist/release/policy.d.ts +45 -0
- package/dist/release/policy.js +124 -0
- package/dist/release/result/digest.d.ts +127 -0
- package/dist/release/result/digest.js +140 -0
- package/dist/release/result/spec.d.ts +4 -0
- package/dist/release/result/spec.js +5 -0
- package/dist/release/rollback/digest.d.ts +126 -0
- package/dist/release/rollback/digest.js +92 -0
- package/dist/release/rollback/spec.d.ts +1729 -0
- package/dist/release/rollback/spec.js +156 -0
- package/dist/release/route/digest.d.ts +45 -0
- package/dist/release/route/digest.js +59 -0
- package/dist/release/route/page.d.ts +56 -0
- package/dist/release/route/page.js +46 -0
- package/dist/release/route/spec.d.ts +49 -0
- package/dist/release/route/spec.js +40 -0
- package/dist/release/route/verify.d.ts +66 -0
- package/dist/release/route/verify.js +67 -0
- package/dist/release/signing.d.ts +6 -0
- package/dist/release/signing.js +38 -0
- package/dist/release/snapshot/data.d.ts +475 -0
- package/dist/release/snapshot/data.js +74 -0
- package/dist/release/snapshot/evidence-requirement.d.ts +22 -0
- package/dist/release/snapshot/evidence-requirement.js +21 -0
- package/dist/release/snapshot/evidence.d.ts +58 -0
- package/dist/release/snapshot/evidence.js +14 -0
- package/dist/release/snapshot/program-evidence.d.ts +17 -0
- package/dist/release/snapshot/program-evidence.js +39 -0
- package/dist/release/snapshot/quran-evidence.d.ts +27 -0
- package/dist/release/snapshot/quran-evidence.js +43 -0
- package/dist/release/snapshot/scope.d.ts +60 -0
- package/dist/release/snapshot/scope.js +102 -0
- package/dist/release/snapshot/spec.d.ts +193 -0
- package/dist/release/snapshot/spec.js +139 -0
- package/dist/release/snapshot/tryout-evidence.d.ts +22 -0
- package/dist/release/snapshot/tryout-evidence.js +86 -0
- package/dist/release/snapshot/verify.d.ts +710 -0
- package/dist/release/snapshot/verify.js +147 -0
- package/dist/release/spec.d.ts +491 -0
- package/dist/release/spec.js +200 -0
- package/dist/release/verify.d.ts +98 -0
- package/dist/release/verify.js +70 -0
- package/dist/renderer/compatibility.d.ts +35 -0
- package/dist/renderer/compatibility.js +48 -0
- package/dist/renderer/component.d.ts +99 -0
- package/dist/renderer/component.js +102 -0
- package/dist/renderer/contract.d.ts +144 -0
- package/dist/renderer/contract.js +122 -0
- package/dist/renderer/domain.d.ts +6 -0
- package/dist/renderer/domain.js +18 -0
- package/dist/renderer/manifest.d.ts +86 -0
- package/dist/renderer/manifest.js +49 -0
- package/dist/renderer/selection.d.ts +39 -0
- package/dist/renderer/selection.js +87 -0
- package/dist/runtime/error.d.ts +8 -0
- package/dist/runtime/error.js +20 -0
- package/dist/runtime/protected/limits.d.ts +10 -0
- package/dist/runtime/protected/limits.js +15 -0
- package/dist/runtime/protected/spec.d.ts +328 -0
- package/dist/runtime/protected/spec.js +79 -0
- package/dist/runtime/protected/verify.d.ts +97 -0
- package/dist/runtime/protected/verify.js +72 -0
- package/dist/runtime/result.d.ts +12 -0
- package/dist/runtime/result.js +17 -0
- package/dist/runtime/spec.d.ts +705 -0
- package/dist/runtime/spec.js +61 -0
- package/dist/runtime/verify.d.ts +479 -0
- package/dist/runtime/verify.js +123 -0
- package/dist/signature/spec.d.ts +53 -0
- package/dist/signature/spec.js +29 -0
- package/dist/signature/trusted.d.ts +19 -0
- package/dist/signature/trusted.js +32 -0
- package/dist/signature/verify.d.ts +10 -0
- package/dist/signature/verify.js +82 -0
- package/dist/text/order.d.ts +2 -0
- package/dist/text/order.js +10 -0
- package/dist/text/syntax.d.ts +6 -0
- package/dist/text/syntax.js +15 -0
- package/dist/transport/batch.d.ts +971 -0
- package/dist/transport/batch.js +101 -0
- package/dist/transport/failure.d.ts +168 -0
- package/dist/transport/failure.js +194 -0
- package/dist/transport/group.d.ts +2946 -0
- package/dist/transport/group.js +47 -0
- package/dist/transport/limits.d.ts +30 -0
- package/dist/transport/limits.js +30 -0
- package/dist/transport/request.d.ts +9333 -0
- package/dist/transport/request.js +121 -0
- package/dist/transport/response.d.ts +6247 -0
- package/dist/transport/response.js +181 -0
- package/dist/transport/runtime.d.ts +77 -0
- package/dist/transport/runtime.js +33 -0
- package/dist/transport/snapshot.d.ts +978 -0
- package/dist/transport/snapshot.js +70 -0
- package/dist/tryout/canonical.d.ts +2 -0
- package/dist/tryout/canonical.js +11 -0
- package/dist/tryout/catalog-hash.d.ts +134 -0
- package/dist/tryout/catalog-hash.js +203 -0
- package/dist/tryout/catalog.d.ts +376 -0
- package/dist/tryout/catalog.js +147 -0
- package/dist/tryout/content-hash.d.ts +89 -0
- package/dist/tryout/content-hash.js +72 -0
- package/dist/tryout/identity.d.ts +12 -0
- package/dist/tryout/identity.js +46 -0
- package/dist/tryout/key.d.ts +4 -0
- package/dist/tryout/key.js +8 -0
- package/dist/tryout/language.d.ts +24 -0
- package/dist/tryout/language.js +32 -0
- package/dist/tryout/locale-closure.d.ts +20 -0
- package/dist/tryout/locale-closure.js +164 -0
- package/dist/tryout/placement-closure.d.ts +5 -0
- package/dist/tryout/placement-closure.js +30 -0
- package/dist/tryout/placement-hash.d.ts +11 -0
- package/dist/tryout/placement-hash.js +55 -0
- package/dist/tryout/placement.d.ts +280 -0
- package/dist/tryout/placement.js +82 -0
- package/dist/tryout/row-hash.d.ts +28 -0
- package/dist/tryout/row-hash.js +50 -0
- package/dist/tryout/runtime/canonical.d.ts +8 -0
- package/dist/tryout/runtime/canonical.js +33 -0
- package/dist/tryout/runtime/hash.d.ts +32 -0
- package/dist/tryout/runtime/hash.js +11 -0
- package/dist/tryout/runtime/source.d.ts +42 -0
- package/dist/tryout/runtime/source.js +47 -0
- package/dist/tryout/runtime/spec.d.ts +58 -0
- package/dist/tryout/runtime/spec.js +21 -0
- package/dist/tryout/runtime/verify.d.ts +61 -0
- package/dist/tryout/runtime/verify.js +69 -0
- package/dist/tryout/snapshot/canonical.d.ts +7 -0
- package/dist/tryout/snapshot/canonical.js +17 -0
- package/dist/tryout/snapshot/hash.d.ts +8 -0
- package/dist/tryout/snapshot/hash.js +19 -0
- package/dist/tryout/snapshot/spec.d.ts +46 -0
- package/dist/tryout/snapshot/spec.js +30 -0
- package/dist/tryout/spec.d.ts +13 -0
- package/dist/tryout/spec.js +15 -0
- package/package.json +506 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ReleaseIdSchema, Sha256HashSchema } from "#contracts/ids";
|
|
3
|
+
import { PublicationReceiptSchema, SignedContentReleaseSchema, } from "#contracts/release/spec";
|
|
4
|
+
import { RendererManifestEnvelopeSchema } from "#contracts/renderer/contract";
|
|
5
|
+
const CleanupCountSchema = Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0)));
|
|
6
|
+
/** Maximum rows the backend may delete in one cleanup transaction. */
|
|
7
|
+
export const MAX_CLEANUP_PAGE_COUNT = 100;
|
|
8
|
+
const CleanupRetrySchema = Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0)), Schema.check(Schema.isFinite()));
|
|
9
|
+
/** Exact immutable release identity requested from durable target state. */
|
|
10
|
+
export const ContentReleaseStatusRequestSchema = Schema.Struct({
|
|
11
|
+
manifestHash: Sha256HashSchema,
|
|
12
|
+
releaseId: ReleaseIdSchema,
|
|
13
|
+
});
|
|
14
|
+
/** Frozen renderer and signed release required for exact crash recovery. */
|
|
15
|
+
export const ContentReleaseBundleSchema = Schema.Struct({
|
|
16
|
+
release: SignedContentReleaseSchema,
|
|
17
|
+
rendererManifest: RendererManifestEnvelopeSchema,
|
|
18
|
+
}).pipe(Schema.check(Schema.makeFilter((bundle) => bundle.release.manifest.rendererManifestHash ===
|
|
19
|
+
bundle.rendererManifest.hash &&
|
|
20
|
+
bundle.release.manifest.rendererContractVersion ===
|
|
21
|
+
bundle.rendererManifest.rendererContractVersion, {
|
|
22
|
+
message: "Expected the signed release to bind the frozen renderer envelope.",
|
|
23
|
+
})));
|
|
24
|
+
/** Exact renderer-bound bundle accepted only for recovery publication. */
|
|
25
|
+
export const RollbackContentReleaseBundleSchema = ContentReleaseBundleSchema.pipe(Schema.refine((bundle) => bundle.release.manifest.origin.kind === "rollback", { message: "Expected a renderer-bound rollback release." }));
|
|
26
|
+
const ContentReleaseStatusIdentity = {
|
|
27
|
+
manifestHash: Sha256HashSchema,
|
|
28
|
+
releaseId: ReleaseIdSchema,
|
|
29
|
+
};
|
|
30
|
+
/** Checks completed evidence against the durable status identity. */
|
|
31
|
+
function hasBoundReceipt(status) {
|
|
32
|
+
return (status.receipt.releaseId === status.releaseId &&
|
|
33
|
+
status.receipt.manifestHash === status.manifestHash);
|
|
34
|
+
}
|
|
35
|
+
const CompletedReleaseStatusSchema = Schema.Struct({
|
|
36
|
+
phase: Schema.Literal("completed"),
|
|
37
|
+
receipt: PublicationReceiptSchema,
|
|
38
|
+
...ContentReleaseStatusIdentity,
|
|
39
|
+
}).pipe(Schema.check(Schema.makeFilter(hasBoundReceipt, {
|
|
40
|
+
message: "Expected the completed receipt to match the release status identity.",
|
|
41
|
+
})));
|
|
42
|
+
/** Persisted release phase bound to one exact signed manifest identity. */
|
|
43
|
+
export const ContentReleaseStatusSchema = Schema.Union([
|
|
44
|
+
Schema.Struct({
|
|
45
|
+
phase: Schema.Literal("missing"),
|
|
46
|
+
...ContentReleaseStatusIdentity,
|
|
47
|
+
}),
|
|
48
|
+
Schema.Struct({
|
|
49
|
+
phase: Schema.Literals([
|
|
50
|
+
"staging",
|
|
51
|
+
"verifying",
|
|
52
|
+
"verified",
|
|
53
|
+
"aborting",
|
|
54
|
+
"aborted",
|
|
55
|
+
]),
|
|
56
|
+
...ContentReleaseStatusIdentity,
|
|
57
|
+
}),
|
|
58
|
+
CompletedReleaseStatusSchema,
|
|
59
|
+
]);
|
|
60
|
+
/** One invisible staged release whose rows should be abandoned. */
|
|
61
|
+
export const ReleaseAbortRequestSchema = Schema.Struct({
|
|
62
|
+
releaseId: ReleaseIdSchema,
|
|
63
|
+
});
|
|
64
|
+
/** Identifies the active release and retained inverse accepted by an operator. */
|
|
65
|
+
export const ReleaseAcceptRequestSchema = Schema.Struct({
|
|
66
|
+
recoveryId: ReleaseIdSchema,
|
|
67
|
+
releaseId: ReleaseIdSchema,
|
|
68
|
+
}).pipe(Schema.check(Schema.makeFilter((request) => request.recoveryId !== request.releaseId, {
|
|
69
|
+
message: "Expected distinct active and recovery release identities.",
|
|
70
|
+
})));
|
|
71
|
+
/** Checks cumulative abort evidence against its durable total. */
|
|
72
|
+
function hasCoherentAbortReceipt(receipt) {
|
|
73
|
+
return (receipt.processedItems <= receipt.totalItems &&
|
|
74
|
+
receipt.complete === (receipt.processedItems === receipt.totalItems));
|
|
75
|
+
}
|
|
76
|
+
/** Durable cumulative evidence from one server-owned abort page. */
|
|
77
|
+
export const ReleaseAbortReceiptSchema = Schema.Struct({
|
|
78
|
+
complete: Schema.Boolean,
|
|
79
|
+
processedItems: CleanupCountSchema,
|
|
80
|
+
releaseId: ReleaseIdSchema,
|
|
81
|
+
totalItems: CleanupCountSchema,
|
|
82
|
+
}).pipe(Schema.check(Schema.makeFilter(hasCoherentAbortReceipt, {
|
|
83
|
+
message: "Expected abort progress to match its durable release item total.",
|
|
84
|
+
})));
|
|
85
|
+
/** One release whose unreachable rows should advance through server-owned state. */
|
|
86
|
+
export const ReleaseCleanupRequestSchema = Schema.Struct({
|
|
87
|
+
releaseId: ReleaseIdSchema,
|
|
88
|
+
});
|
|
89
|
+
/** Durable cumulative evidence returned after server-owned cleanup progress. */
|
|
90
|
+
export const ReleaseCleanupReceiptSchema = Schema.Struct({
|
|
91
|
+
complete: Schema.Boolean,
|
|
92
|
+
deletedArtifacts: CleanupCountSchema,
|
|
93
|
+
releaseId: ReleaseIdSchema,
|
|
94
|
+
retryAt: Schema.optional(CleanupRetrySchema),
|
|
95
|
+
}).pipe(Schema.check(Schema.makeFilter((receipt) => !(receipt.complete && receipt.retryAt !== undefined), {
|
|
96
|
+
message: "Expected completed cleanup evidence without a retry timestamp.",
|
|
97
|
+
})));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Exact reviewed Git source used by a normal reproducible release. */
|
|
3
|
+
export declare const GitReleaseOriginSchema: Schema.Struct<{
|
|
4
|
+
readonly kind: Schema.Literal<"git">;
|
|
5
|
+
readonly sha: Schema.brand<Schema.String, "@NakafaAI/AksaraGitCommitSha">;
|
|
6
|
+
}>;
|
|
7
|
+
/** Exact active release whose prior state is restored by a forward rollback. */
|
|
8
|
+
export declare const RollbackReleaseOriginSchema: Schema.Struct<{
|
|
9
|
+
readonly kind: Schema.Literal<"rollback">;
|
|
10
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
11
|
+
}>;
|
|
12
|
+
/** Signed provenance for either exact-Git compilation or artifact reuse. */
|
|
13
|
+
export declare const ReleaseOriginSchema: Schema.Union<readonly [Schema.Struct<{
|
|
14
|
+
readonly kind: Schema.Literal<"git">;
|
|
15
|
+
readonly sha: Schema.brand<Schema.String, "@NakafaAI/AksaraGitCommitSha">;
|
|
16
|
+
}>, Schema.Struct<{
|
|
17
|
+
readonly kind: Schema.Literal<"rollback">;
|
|
18
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
19
|
+
}>]>;
|
|
20
|
+
export type ReleaseOrigin = typeof ReleaseOriginSchema.Type;
|
|
21
|
+
/** Copies one release origin into deterministic manifest field order. */
|
|
22
|
+
export declare function canonicalizeReleaseOrigin(origin: ReleaseOrigin): {
|
|
23
|
+
kind: "git";
|
|
24
|
+
sha: string & import("effect/Brand").Brand<"@NakafaAI/AksaraGitCommitSha">;
|
|
25
|
+
releaseId?: never;
|
|
26
|
+
} | {
|
|
27
|
+
sha?: never;
|
|
28
|
+
kind: "rollback";
|
|
29
|
+
releaseId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraReleaseId">;
|
|
30
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { GitCommitShaSchema, ReleaseIdSchema } from "#contracts/ids";
|
|
3
|
+
/** Exact reviewed Git source used by a normal reproducible release. */
|
|
4
|
+
export const GitReleaseOriginSchema = Schema.Struct({
|
|
5
|
+
kind: Schema.Literal("git"),
|
|
6
|
+
sha: GitCommitShaSchema,
|
|
7
|
+
});
|
|
8
|
+
/** Exact active release whose prior state is restored by a forward rollback. */
|
|
9
|
+
export const RollbackReleaseOriginSchema = Schema.Struct({
|
|
10
|
+
kind: Schema.Literal("rollback"),
|
|
11
|
+
releaseId: ReleaseIdSchema,
|
|
12
|
+
});
|
|
13
|
+
/** Signed provenance for either exact-Git compilation or artifact reuse. */
|
|
14
|
+
export const ReleaseOriginSchema = Schema.Union([
|
|
15
|
+
GitReleaseOriginSchema,
|
|
16
|
+
RollbackReleaseOriginSchema,
|
|
17
|
+
]);
|
|
18
|
+
/** Copies one release origin into deterministic manifest field order. */
|
|
19
|
+
export function canonicalizeReleaseOrigin(origin) {
|
|
20
|
+
if (origin.kind === "git") {
|
|
21
|
+
return { kind: origin.kind, sha: origin.sha };
|
|
22
|
+
}
|
|
23
|
+
return { kind: origin.kind, releaseId: origin.releaseId };
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Sha256Hash } from "#contracts/ids";
|
|
3
|
+
import type { ContentSnapshotManifest } from "#contracts/release/snapshot/data";
|
|
4
|
+
import { type PublicationScope } from "#contracts/release/snapshot/scope";
|
|
5
|
+
/** Locale policy shared by one release and its structured snapshots. */
|
|
6
|
+
export declare const ReleasePolicySchema: Schema.Struct<{
|
|
7
|
+
readonly activeAppLocales: Schema.NonEmptyArray<Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">>;
|
|
8
|
+
}>;
|
|
9
|
+
export type ReleasePolicy = typeof ReleasePolicySchema.Type;
|
|
10
|
+
declare const ReleasePolicyClosureError_base: Schema.Class<ReleasePolicyClosureError, Schema.TaggedStruct<"ReleasePolicyClosureError", {
|
|
11
|
+
readonly actual: Schema.String;
|
|
12
|
+
readonly expected: Schema.String;
|
|
13
|
+
readonly family: Schema.Union<readonly [Schema.Literals<readonly ["program", "quran", "tryout"]>, Schema.Literals<readonly ["article", "material", "page", "question"]>]>;
|
|
14
|
+
readonly field: Schema.Literals<readonly ["activeAppLocales", "manifest", "scope"]>;
|
|
15
|
+
}>, import("effect/Cause").YieldableError>;
|
|
16
|
+
/** One structured scope does not close over the current release policy. */
|
|
17
|
+
export declare class ReleasePolicyClosureError extends ReleasePolicyClosureError_base {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Verifies snapshot replacement policy for one release transition.
|
|
21
|
+
*
|
|
22
|
+
* Genesis and policy changes must replace every structured snapshot. An
|
|
23
|
+
* unchanged policy may inherit prior snapshots while validating every supplied
|
|
24
|
+
* replacement against the current release identity.
|
|
25
|
+
*/
|
|
26
|
+
export declare const verifyReleasePolicyTransition: (input: {
|
|
27
|
+
readonly basePolicy: ReleasePolicy | null;
|
|
28
|
+
readonly manifests: readonly ContentSnapshotManifest[];
|
|
29
|
+
readonly policy: ReleasePolicy;
|
|
30
|
+
readonly scope: PublicationScope;
|
|
31
|
+
}) => Effect.Effect<undefined, ReleasePolicyClosureError, never>;
|
|
32
|
+
/**
|
|
33
|
+
* Requires a renderer change to prove every retained content artifact.
|
|
34
|
+
*
|
|
35
|
+
* Selecting every content family inspects the complete desired corpus against
|
|
36
|
+
* the candidate renderer. An active try-out also requires its structured
|
|
37
|
+
* snapshot so the new runtime pair cannot retain superseded artifact hashes.
|
|
38
|
+
*/
|
|
39
|
+
export declare const verifyRendererPolicyTransition: (input: {
|
|
40
|
+
readonly baseRendererManifestHash: Sha256Hash | null;
|
|
41
|
+
readonly baseTryoutSnapshotId: Sha256Hash | null;
|
|
42
|
+
readonly rendererManifestHash: Sha256Hash;
|
|
43
|
+
readonly scope: PublicationScope;
|
|
44
|
+
}) => Effect.Effect<undefined, ReleasePolicyClosureError, never>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ContentFamilySchema } from "#contracts/content";
|
|
3
|
+
import { ActiveAppLocaleListSchema } from "#contracts/locale";
|
|
4
|
+
import { ContentSnapshotKindSchema, } from "#contracts/release/snapshot/scope";
|
|
5
|
+
const ClosureFieldSchema = Schema.Literals([
|
|
6
|
+
"activeAppLocales",
|
|
7
|
+
"manifest",
|
|
8
|
+
"scope",
|
|
9
|
+
]);
|
|
10
|
+
/** Locale policy shared by one release and its structured snapshots. */
|
|
11
|
+
export const ReleasePolicySchema = Schema.Struct({
|
|
12
|
+
activeAppLocales: ActiveAppLocaleListSchema,
|
|
13
|
+
});
|
|
14
|
+
/** One structured scope does not close over the current release policy. */
|
|
15
|
+
export class ReleasePolicyClosureError extends Schema.TaggedError()("ReleasePolicyClosureError", {
|
|
16
|
+
actual: Schema.String,
|
|
17
|
+
expected: Schema.String,
|
|
18
|
+
family: Schema.Union([ContentSnapshotKindSchema, ContentFamilySchema]),
|
|
19
|
+
field: ClosureFieldSchema,
|
|
20
|
+
}) {
|
|
21
|
+
}
|
|
22
|
+
/** Compares locale lists without exchanging their branded element roles. */
|
|
23
|
+
function hasSameLocaleCodes(left, right) {
|
|
24
|
+
return (left.length === right.length &&
|
|
25
|
+
left.every((locale, index) => locale === right[index]));
|
|
26
|
+
}
|
|
27
|
+
/** Returns whether two release policies have the same signed identity. */
|
|
28
|
+
function hasSamePolicy(left, right) {
|
|
29
|
+
return hasSameLocaleCodes(left.activeAppLocales, right.activeAppLocales);
|
|
30
|
+
}
|
|
31
|
+
/** Fails with one exact structured-scope policy mismatch. */
|
|
32
|
+
function failClosure(input) {
|
|
33
|
+
return Effect.fail(new ReleasePolicyClosureError(input));
|
|
34
|
+
}
|
|
35
|
+
/** Verifies one replacement snapshot against the new release policy. */
|
|
36
|
+
function verifyManifestPolicy(manifest, policy) {
|
|
37
|
+
if (!hasSameLocaleCodes(manifest.manifest.activeAppLocales, policy.activeAppLocales)) {
|
|
38
|
+
return failClosure({
|
|
39
|
+
actual: manifest.manifest.activeAppLocales.join(","),
|
|
40
|
+
expected: policy.activeAppLocales.join(","),
|
|
41
|
+
family: manifest.family,
|
|
42
|
+
field: "activeAppLocales",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return Effect.void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Verifies snapshot replacement policy for one release transition.
|
|
49
|
+
*
|
|
50
|
+
* Genesis and policy changes must replace every structured snapshot. An
|
|
51
|
+
* unchanged policy may inherit prior snapshots while validating every supplied
|
|
52
|
+
* replacement against the current release identity.
|
|
53
|
+
*/
|
|
54
|
+
export const verifyReleasePolicyTransition = Effect.fn("AksaraContracts.verifyReleasePolicyTransition")(function* (input) {
|
|
55
|
+
const requiresCompleteReplacement = input.basePolicy === null || !hasSamePolicy(input.basePolicy, input.policy);
|
|
56
|
+
if (requiresCompleteReplacement) {
|
|
57
|
+
for (const family of ContentFamilySchema.literals) {
|
|
58
|
+
if (!input.scope.families.includes(family)) {
|
|
59
|
+
return yield* failClosure({
|
|
60
|
+
actual: "partial",
|
|
61
|
+
expected: "complete-family",
|
|
62
|
+
family,
|
|
63
|
+
field: "scope",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const family of ContentSnapshotKindSchema.literals) {
|
|
69
|
+
const manifests = input.manifests.filter((candidate) => candidate.family === family);
|
|
70
|
+
if (manifests.length > 1) {
|
|
71
|
+
return yield* failClosure({
|
|
72
|
+
actual: String(manifests.length),
|
|
73
|
+
expected: "at-most-one",
|
|
74
|
+
family,
|
|
75
|
+
field: "manifest",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const [manifest] = manifests;
|
|
79
|
+
if (manifest !== undefined) {
|
|
80
|
+
yield* verifyManifestPolicy(manifest, input.policy);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (requiresCompleteReplacement) {
|
|
84
|
+
return yield* failClosure({
|
|
85
|
+
actual: "missing",
|
|
86
|
+
expected: "exactly-one",
|
|
87
|
+
family,
|
|
88
|
+
field: "manifest",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* Requires a renderer change to prove every retained content artifact.
|
|
95
|
+
*
|
|
96
|
+
* Selecting every content family inspects the complete desired corpus against
|
|
97
|
+
* the candidate renderer. An active try-out also requires its structured
|
|
98
|
+
* snapshot so the new runtime pair cannot retain superseded artifact hashes.
|
|
99
|
+
*/
|
|
100
|
+
export const verifyRendererPolicyTransition = Effect.fn("AksaraContracts.verifyRendererPolicyTransition")(function* (input) {
|
|
101
|
+
if (input.baseRendererManifestHash === null ||
|
|
102
|
+
input.baseRendererManifestHash === input.rendererManifestHash) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const family of ContentFamilySchema.literals) {
|
|
106
|
+
if (!input.scope.families.includes(family)) {
|
|
107
|
+
return yield* failClosure({
|
|
108
|
+
actual: "partial",
|
|
109
|
+
expected: "complete-family",
|
|
110
|
+
family,
|
|
111
|
+
field: "scope",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (input.baseTryoutSnapshotId !== null &&
|
|
116
|
+
!input.scope.snapshots.includes("tryout")) {
|
|
117
|
+
return yield* failClosure({
|
|
118
|
+
actual: "missing",
|
|
119
|
+
expected: "renderer-refresh",
|
|
120
|
+
family: "tryout",
|
|
121
|
+
field: "scope",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { type PublicPath, type ReleaseId, Sha256HashSchema } from "#contracts/ids";
|
|
3
|
+
import { type ArtifactLocale } from "#contracts/locale";
|
|
4
|
+
import { type ContentHead } from "#contracts/release/head";
|
|
5
|
+
declare const ResultCatalogHashError_base: Schema.Class<ResultCatalogHashError, Schema.TaggedStruct<"ResultCatalogHashError", {
|
|
6
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
7
|
+
}>, import("effect/Cause").YieldableError>;
|
|
8
|
+
/** SHA-256 computation failed before result-catalog integrity was established. */
|
|
9
|
+
export declare class ResultCatalogHashError extends ResultCatalogHashError_base {
|
|
10
|
+
}
|
|
11
|
+
declare const ResultCatalogOrderError_base: Schema.Class<ResultCatalogOrderError, Schema.TaggedStruct<"ResultCatalogOrderError", {
|
|
12
|
+
readonly artifactLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraArtifactLocale">;
|
|
13
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
14
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
15
|
+
}>, import("effect/Cause").YieldableError>;
|
|
16
|
+
/** A result catalog is duplicated or outside canonical content-head order. */
|
|
17
|
+
export declare class ResultCatalogOrderError extends ResultCatalogOrderError_base {
|
|
18
|
+
}
|
|
19
|
+
declare const ResultCatalogRouteError_base: Schema.Class<ResultCatalogRouteError, Schema.TaggedStruct<"ResultCatalogRouteError", {
|
|
20
|
+
readonly artifactLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraArtifactLocale">;
|
|
21
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
22
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
23
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
24
|
+
}>, import("effect/Cause").YieldableError>;
|
|
25
|
+
/** Two complete result heads claim the same locale-specific public route. */
|
|
26
|
+
export declare class ResultCatalogRouteError extends ResultCatalogRouteError_base {
|
|
27
|
+
}
|
|
28
|
+
declare const ResultCatalogCountMismatchError_base: Schema.Class<ResultCatalogCountMismatchError, Schema.TaggedStruct<"ResultCatalogCountMismatchError", {
|
|
29
|
+
readonly actualCount: Schema.Finite;
|
|
30
|
+
readonly expectedCount: Schema.Finite;
|
|
31
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
32
|
+
}>, import("effect/Cause").YieldableError>;
|
|
33
|
+
/** A replayed result catalog has a different signed head count. */
|
|
34
|
+
export declare class ResultCatalogCountMismatchError extends ResultCatalogCountMismatchError_base {
|
|
35
|
+
}
|
|
36
|
+
declare const ResultCatalogDigestMismatchError_base: Schema.Class<ResultCatalogDigestMismatchError, Schema.TaggedStruct<"ResultCatalogDigestMismatchError", {
|
|
37
|
+
readonly actualDigest: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
|
|
38
|
+
readonly expectedDigest: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
|
|
39
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
40
|
+
}>, import("effect/Cause").YieldableError>;
|
|
41
|
+
/** A replayed result catalog does not match its signed digest. */
|
|
42
|
+
export declare class ResultCatalogDigestMismatchError extends ResultCatalogDigestMismatchError_base {
|
|
43
|
+
}
|
|
44
|
+
/** Keeps mutable hash and ordering state private to one catalog replay. */
|
|
45
|
+
declare class ResultCatalogDigestState {
|
|
46
|
+
#private;
|
|
47
|
+
count: number;
|
|
48
|
+
previous: ContentHead | undefined;
|
|
49
|
+
/** Initializes one domain-separated result-catalog hash. */
|
|
50
|
+
constructor();
|
|
51
|
+
/** Claims one public route and reports whether it was globally unique. */
|
|
52
|
+
claimRoute(artifactLocale: ArtifactLocale, publicPath: PublicPath): boolean;
|
|
53
|
+
/** Adds one canonical compact head and advances ordering evidence. */
|
|
54
|
+
update(head: ContentHead): void;
|
|
55
|
+
/** Consumes the hash and returns its branded immutable identity. */
|
|
56
|
+
digest(): string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
57
|
+
}
|
|
58
|
+
/** Creates a fresh domain-separated digest state for one result catalog. */
|
|
59
|
+
export declare function createResultCatalogDigest(releaseId: ReleaseId): Effect.Effect<ResultCatalogDigestState, ResultCatalogHashError, never>;
|
|
60
|
+
/** Adds one canonically ordered compact head to a result digest. */
|
|
61
|
+
export declare function updateResultCatalogDigest(releaseId: ReleaseId, state: ResultCatalogDigestState, head: ContentHead): Effect.Effect<ResultCatalogDigestState, ResultCatalogHashError | ResultCatalogOrderError | ResultCatalogRouteError>;
|
|
62
|
+
/** Finalizes one incremental result-catalog digest. */
|
|
63
|
+
export declare function finalizeResultCatalogDigest(releaseId: ReleaseId, state: ResultCatalogDigestState): Effect.Effect<string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">, ResultCatalogHashError, never>;
|
|
64
|
+
/** Digests one complete compact-head stream without retaining the catalog. */
|
|
65
|
+
export declare const digestResultCatalog: <E, R>(releaseId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraReleaseId">, heads: Stream.Stream<{
|
|
66
|
+
readonly artifactHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
67
|
+
readonly artifactLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraArtifactLocale">;
|
|
68
|
+
readonly compilerConfigHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
69
|
+
readonly contentKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraContentKey">;
|
|
70
|
+
readonly delivery: "authenticated" | "entitled" | "public";
|
|
71
|
+
readonly projectionHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
72
|
+
readonly publicPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
|
|
73
|
+
readonly rendererDomain: "ai-ds" | "biology" | "chemistry" | "mathematics" | "physics" | "politics" | "site" | "snbt-general" | "snbt-math" | "snbt-plain" | "snbt-quant" | "tka-math";
|
|
74
|
+
readonly sourceHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
75
|
+
readonly sourcePath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraCorpusSourcePath">;
|
|
76
|
+
readonly family: "page";
|
|
77
|
+
} | {
|
|
78
|
+
readonly artifactHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
79
|
+
readonly artifactLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraArtifactLocale">;
|
|
80
|
+
readonly compilerConfigHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
81
|
+
readonly contentKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraContentKey">;
|
|
82
|
+
readonly delivery: "authenticated" | "entitled" | "public";
|
|
83
|
+
readonly projectionHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
84
|
+
readonly publicPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
|
|
85
|
+
readonly rendererDomain: "ai-ds" | "biology" | "chemistry" | "mathematics" | "physics" | "politics" | "site" | "snbt-general" | "snbt-math" | "snbt-plain" | "snbt-quant" | "tka-math";
|
|
86
|
+
readonly sourceHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
87
|
+
readonly sourcePath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraCorpusSourcePath">;
|
|
88
|
+
readonly family: "question";
|
|
89
|
+
} | {
|
|
90
|
+
readonly artifactHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
91
|
+
readonly artifactLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraArtifactLocale">;
|
|
92
|
+
readonly compilerConfigHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
93
|
+
readonly contentKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraContentKey">;
|
|
94
|
+
readonly delivery: "authenticated" | "entitled" | "public";
|
|
95
|
+
readonly projectionHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
96
|
+
readonly publicPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
|
|
97
|
+
readonly rendererDomain: "ai-ds" | "biology" | "chemistry" | "mathematics" | "physics" | "politics" | "site" | "snbt-general" | "snbt-math" | "snbt-plain" | "snbt-quant" | "tka-math";
|
|
98
|
+
readonly sourceHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
99
|
+
readonly sourcePath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraCorpusSourcePath">;
|
|
100
|
+
readonly family: "article";
|
|
101
|
+
} | {
|
|
102
|
+
readonly artifactHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
103
|
+
readonly artifactLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraArtifactLocale">;
|
|
104
|
+
readonly compilerConfigHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
105
|
+
readonly contentKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraContentKey">;
|
|
106
|
+
readonly delivery: "authenticated" | "entitled" | "public";
|
|
107
|
+
readonly projectionHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
108
|
+
readonly publicPath?: (string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">) | undefined;
|
|
109
|
+
readonly rendererDomain: "ai-ds" | "biology" | "chemistry" | "mathematics" | "physics" | "politics" | "site" | "snbt-general" | "snbt-math" | "snbt-plain" | "snbt-quant" | "tka-math";
|
|
110
|
+
readonly sourceHash: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
111
|
+
readonly sourcePath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraCorpusSourcePath">;
|
|
112
|
+
readonly family: "material";
|
|
113
|
+
}, E, R>) => Effect.Effect<{
|
|
114
|
+
count: number;
|
|
115
|
+
digest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
116
|
+
}, E | ResultCatalogHashError | ResultCatalogOrderError | ResultCatalogRouteError, R>;
|
|
117
|
+
/** Authenticates one complete replayed catalog against its signed result root. */
|
|
118
|
+
export declare const verifyResultCatalog: <E, R>(input: {
|
|
119
|
+
readonly expectedCount: number;
|
|
120
|
+
readonly expectedDigest: typeof Sha256HashSchema.Type;
|
|
121
|
+
readonly heads: Stream.Stream<ContentHead, E, R>;
|
|
122
|
+
readonly releaseId: ReleaseId;
|
|
123
|
+
}) => Effect.Effect<{
|
|
124
|
+
count: number;
|
|
125
|
+
digest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
126
|
+
}, E | ResultCatalogCountMismatchError | ResultCatalogDigestMismatchError | ResultCatalogHashError | ResultCatalogOrderError | ResultCatalogRouteError, R>;
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Effect, Schema, Stream } from "effect";
|
|
3
|
+
import { compareContentHeads, routeIdentity } from "#contracts/content";
|
|
4
|
+
import { ContentKeySchema, PublicPathSchema, ReleaseIdSchema, Sha256HashSchema, } from "#contracts/ids";
|
|
5
|
+
import { AppLocaleSchema, ArtifactLocaleSchema, } from "#contracts/locale";
|
|
6
|
+
import { canonicalizeContentHead, } from "#contracts/release/head";
|
|
7
|
+
import { RESULT_CATALOG_DIGEST_DOMAIN } from "#contracts/release/result/spec";
|
|
8
|
+
/** SHA-256 computation failed before result-catalog integrity was established. */
|
|
9
|
+
export class ResultCatalogHashError extends Schema.TaggedError()("ResultCatalogHashError", { releaseId: ReleaseIdSchema }) {
|
|
10
|
+
}
|
|
11
|
+
/** A result catalog is duplicated or outside canonical content-head order. */
|
|
12
|
+
export class ResultCatalogOrderError extends Schema.TaggedError()("ResultCatalogOrderError", {
|
|
13
|
+
artifactLocale: ArtifactLocaleSchema,
|
|
14
|
+
contentKey: ContentKeySchema,
|
|
15
|
+
releaseId: ReleaseIdSchema,
|
|
16
|
+
}) {
|
|
17
|
+
}
|
|
18
|
+
/** Two complete result heads claim the same locale-specific public route. */
|
|
19
|
+
export class ResultCatalogRouteError extends Schema.TaggedError()("ResultCatalogRouteError", {
|
|
20
|
+
artifactLocale: ArtifactLocaleSchema,
|
|
21
|
+
contentKey: ContentKeySchema,
|
|
22
|
+
publicPath: PublicPathSchema,
|
|
23
|
+
releaseId: ReleaseIdSchema,
|
|
24
|
+
}) {
|
|
25
|
+
}
|
|
26
|
+
/** A replayed result catalog has a different signed head count. */
|
|
27
|
+
export class ResultCatalogCountMismatchError extends Schema.TaggedError()("ResultCatalogCountMismatchError", {
|
|
28
|
+
actualCount: Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
29
|
+
expectedCount: Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
30
|
+
releaseId: ReleaseIdSchema,
|
|
31
|
+
}) {
|
|
32
|
+
}
|
|
33
|
+
/** A replayed result catalog does not match its signed digest. */
|
|
34
|
+
export class ResultCatalogDigestMismatchError extends Schema.TaggedError()("ResultCatalogDigestMismatchError", {
|
|
35
|
+
actualDigest: Sha256HashSchema,
|
|
36
|
+
expectedDigest: Sha256HashSchema,
|
|
37
|
+
releaseId: ReleaseIdSchema,
|
|
38
|
+
}) {
|
|
39
|
+
}
|
|
40
|
+
/** Keeps mutable hash and ordering state private to one catalog replay. */
|
|
41
|
+
class ResultCatalogDigestState {
|
|
42
|
+
#hash = createHash("sha256");
|
|
43
|
+
#routes = new Set();
|
|
44
|
+
count = 0;
|
|
45
|
+
previous;
|
|
46
|
+
/** Initializes one domain-separated result-catalog hash. */
|
|
47
|
+
constructor() {
|
|
48
|
+
this.#hash.update(RESULT_CATALOG_DIGEST_DOMAIN);
|
|
49
|
+
this.#hash.update("\n");
|
|
50
|
+
}
|
|
51
|
+
/** Claims one public route and reports whether it was globally unique. */
|
|
52
|
+
claimRoute(artifactLocale, publicPath) {
|
|
53
|
+
const identity = routeIdentity({
|
|
54
|
+
appLocale: AppLocaleSchema.make(artifactLocale),
|
|
55
|
+
publicPath,
|
|
56
|
+
});
|
|
57
|
+
if (this.#routes.has(identity)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
this.#routes.add(identity);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/** Adds one canonical compact head and advances ordering evidence. */
|
|
64
|
+
update(head) {
|
|
65
|
+
this.#hash.update(canonicalizeContentHead(head));
|
|
66
|
+
this.#hash.update("\n");
|
|
67
|
+
this.count += 1;
|
|
68
|
+
this.previous = head;
|
|
69
|
+
}
|
|
70
|
+
/** Consumes the hash and returns its branded immutable identity. */
|
|
71
|
+
digest() {
|
|
72
|
+
return Sha256HashSchema.make(`sha256:${this.#hash.digest("hex")}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Creates a fresh domain-separated digest state for one result catalog. */
|
|
76
|
+
export function createResultCatalogDigest(releaseId) {
|
|
77
|
+
return Effect.try({
|
|
78
|
+
catch: () => new ResultCatalogHashError({ releaseId }),
|
|
79
|
+
try: () => new ResultCatalogDigestState(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Adds one canonically ordered compact head to a result digest. */
|
|
83
|
+
export function updateResultCatalogDigest(releaseId, state, head) {
|
|
84
|
+
if (state.previous && compareContentHeads(state.previous, head) >= 0) {
|
|
85
|
+
return Effect.fail(new ResultCatalogOrderError({
|
|
86
|
+
artifactLocale: head.artifactLocale,
|
|
87
|
+
contentKey: head.contentKey,
|
|
88
|
+
releaseId,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
if (head.publicPath !== undefined &&
|
|
92
|
+
!state.claimRoute(head.artifactLocale, head.publicPath)) {
|
|
93
|
+
return Effect.fail(new ResultCatalogRouteError({
|
|
94
|
+
artifactLocale: head.artifactLocale,
|
|
95
|
+
contentKey: head.contentKey,
|
|
96
|
+
publicPath: head.publicPath,
|
|
97
|
+
releaseId,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
return Effect.try({
|
|
101
|
+
catch: () => new ResultCatalogHashError({ releaseId }),
|
|
102
|
+
try: () => {
|
|
103
|
+
state.update(head);
|
|
104
|
+
return state;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** Finalizes one incremental result-catalog digest. */
|
|
109
|
+
export function finalizeResultCatalogDigest(releaseId, state) {
|
|
110
|
+
return Effect.try({
|
|
111
|
+
catch: () => new ResultCatalogHashError({ releaseId }),
|
|
112
|
+
try: () => state.digest(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Digests one complete compact-head stream without retaining the catalog. */
|
|
116
|
+
export const digestResultCatalog = Effect.fn("AksaraContracts.digestResultCatalog")(function* (releaseId, heads) {
|
|
117
|
+
const initial = yield* createResultCatalogDigest(releaseId);
|
|
118
|
+
const state = yield* heads.pipe(Stream.runFoldEffect(() => initial, (current, head) => updateResultCatalogDigest(releaseId, current, head)));
|
|
119
|
+
const digest = yield* finalizeResultCatalogDigest(releaseId, state);
|
|
120
|
+
return { count: state.count, digest };
|
|
121
|
+
});
|
|
122
|
+
/** Authenticates one complete replayed catalog against its signed result root. */
|
|
123
|
+
export const verifyResultCatalog = Effect.fn("AksaraContracts.verifyResultCatalog")(function* (input) {
|
|
124
|
+
const summary = yield* digestResultCatalog(input.releaseId, input.heads);
|
|
125
|
+
if (summary.count !== input.expectedCount) {
|
|
126
|
+
return yield* new ResultCatalogCountMismatchError({
|
|
127
|
+
actualCount: summary.count,
|
|
128
|
+
expectedCount: input.expectedCount,
|
|
129
|
+
releaseId: input.releaseId,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (summary.digest !== input.expectedDigest) {
|
|
133
|
+
return yield* new ResultCatalogDigestMismatchError({
|
|
134
|
+
actualDigest: summary.digest,
|
|
135
|
+
expectedDigest: input.expectedDigest,
|
|
136
|
+
releaseId: input.releaseId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return summary;
|
|
140
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Domain separator shared by canonical result-catalog digest implementations. */
|
|
2
|
+
export declare const RESULT_CATALOG_DIGEST_DOMAIN = "nakafa.aksara.result-catalog";
|
|
3
|
+
/** Canonical signed root for a release whose complete result catalog is empty. */
|
|
4
|
+
export declare const EMPTY_RESULT_CATALOG_DIGEST: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Sha256HashSchema } from "#contracts/ids";
|
|
2
|
+
/** Domain separator shared by canonical result-catalog digest implementations. */
|
|
3
|
+
export const RESULT_CATALOG_DIGEST_DOMAIN = "nakafa.aksara.result-catalog";
|
|
4
|
+
/** Canonical signed root for a release whose complete result catalog is empty. */
|
|
5
|
+
export const EMPTY_RESULT_CATALOG_DIGEST = Sha256HashSchema.make("sha256:ed7d49e237dadbd311a1599264b00852ae18657d123c8f9cbc26c1c62c8f81cd");
|