@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,156 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ContentFamilySchema, canonicalizeSignedContentArtifact, SignedContentArtifactSchema, } from "#contracts/content";
|
|
3
|
+
import { ContentKeySchema, ReleaseIdSchema, Sha256HashSchema, } from "#contracts/ids";
|
|
4
|
+
import { ContentProjectionSchema, canonicalizeContentProjection, familyForProjection, projectionArtifactLocale, } from "#contracts/projection/spec";
|
|
5
|
+
import { canonicalizeContentChange } from "#contracts/release/canonical";
|
|
6
|
+
import { ArticleHeadSchema, canonicalizeContentHead, MaterialHeadSchema, PageHeadSchema, QuestionHeadSchema, } from "#contracts/release/head";
|
|
7
|
+
import { ContentDeleteSchema, ContentUpsertSchema, ReleaseItemIndexSchema, } from "#contracts/release/spec";
|
|
8
|
+
/** Maximum complete body rollback page below the publication wire ceiling. */
|
|
9
|
+
export const MAX_ROLLBACK_PAGE_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
/** Maximum byte-bounded rollback transitions requested from one target page. */
|
|
11
|
+
export const MAX_ROLLBACK_PAGE_RECORDS = 512;
|
|
12
|
+
const RollbackCursorSchema = Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(-1)));
|
|
13
|
+
/** Explicit proof that a changed content head did not previously exist. */
|
|
14
|
+
export const RollbackAbsentStateSchema = Schema.Struct({
|
|
15
|
+
artifactLocale: ContentUpsertSchema.fields.artifactLocale,
|
|
16
|
+
contentKey: ContentKeySchema,
|
|
17
|
+
family: ContentFamilySchema,
|
|
18
|
+
state: Schema.Literal("absent"),
|
|
19
|
+
});
|
|
20
|
+
/** Compact authoritative prior article head protected by one rollback digest. */
|
|
21
|
+
export const RollbackArticleStateSchema = Schema.Struct({
|
|
22
|
+
head: ArticleHeadSchema,
|
|
23
|
+
state: Schema.Literal("article"),
|
|
24
|
+
});
|
|
25
|
+
/** Compact authoritative prior head protected by one rollback snapshot digest. */
|
|
26
|
+
export const RollbackMaterialStateSchema = Schema.Struct({
|
|
27
|
+
head: MaterialHeadSchema,
|
|
28
|
+
state: Schema.Literal("material"),
|
|
29
|
+
});
|
|
30
|
+
/** Compact authoritative prior page head protected by one rollback digest. */
|
|
31
|
+
export const RollbackPageStateSchema = Schema.Struct({
|
|
32
|
+
head: PageHeadSchema,
|
|
33
|
+
state: Schema.Literal("page"),
|
|
34
|
+
});
|
|
35
|
+
/** Compact authoritative prior question head protected by one rollback digest. */
|
|
36
|
+
export const RollbackQuestionStateSchema = Schema.Struct({
|
|
37
|
+
head: QuestionHeadSchema,
|
|
38
|
+
state: Schema.Literal("question"),
|
|
39
|
+
});
|
|
40
|
+
/** Complete compact state vocabulary authenticated for one future rollback. */
|
|
41
|
+
export const RollbackSnapshotStateSchema = Schema.Union([
|
|
42
|
+
RollbackAbsentStateSchema,
|
|
43
|
+
RollbackArticleStateSchema,
|
|
44
|
+
RollbackMaterialStateSchema,
|
|
45
|
+
RollbackPageStateSchema,
|
|
46
|
+
RollbackQuestionStateSchema,
|
|
47
|
+
]);
|
|
48
|
+
/** One ordered prior state covered by a signed release's rollback digest. */
|
|
49
|
+
export const RollbackSnapshotEntrySchema = Schema.Struct({
|
|
50
|
+
index: ReleaseItemIndexSchema,
|
|
51
|
+
releaseId: ReleaseIdSchema,
|
|
52
|
+
snapshot: RollbackSnapshotStateSchema,
|
|
53
|
+
});
|
|
54
|
+
/** Serializes one authenticated prior state for incremental digest computation. */
|
|
55
|
+
export function canonicalizeRollbackSnapshotEntry(entry) {
|
|
56
|
+
const snapshot = entry.snapshot.state === "absent"
|
|
57
|
+
? `{"artifactLocale":${JSON.stringify(entry.snapshot.artifactLocale)},"contentKey":${JSON.stringify(entry.snapshot.contentKey)},"family":${JSON.stringify(entry.snapshot.family)},"state":"absent"}`
|
|
58
|
+
: `{"head":${canonicalizeContentHead(entry.snapshot.head)},"state":${JSON.stringify(entry.snapshot.state)}}`;
|
|
59
|
+
return `{"index":${entry.index},"releaseId":${JSON.stringify(entry.releaseId)},"snapshot":${snapshot}}`;
|
|
60
|
+
}
|
|
61
|
+
/** Checks identity coherence across one upsert, artifact, and projection. */
|
|
62
|
+
function hasBoundRollbackUpsert(input) {
|
|
63
|
+
const { artifact, change, projection } = input;
|
|
64
|
+
const { payload } = artifact;
|
|
65
|
+
return (artifact.artifactHash === change.artifactHash &&
|
|
66
|
+
payload.contentKey === change.contentKey &&
|
|
67
|
+
payload.artifactLocale === change.artifactLocale &&
|
|
68
|
+
payload.rendererDomain === change.rendererDomain &&
|
|
69
|
+
projection.contentKey === change.contentKey &&
|
|
70
|
+
projectionArtifactLocale(projection) === change.artifactLocale &&
|
|
71
|
+
familyForProjection(projection) === change.family);
|
|
72
|
+
}
|
|
73
|
+
/** Complete body-bearing state for one existing material head. */
|
|
74
|
+
export const RollbackUpsertStateSchema = Schema.Struct({
|
|
75
|
+
artifact: SignedContentArtifactSchema,
|
|
76
|
+
change: ContentUpsertSchema,
|
|
77
|
+
projection: ContentProjectionSchema,
|
|
78
|
+
}).pipe(Schema.check(Schema.makeFilter(hasBoundRollbackUpsert, {
|
|
79
|
+
message: "Expected rollback change, artifact, and projection identities to match.",
|
|
80
|
+
})));
|
|
81
|
+
/** Body-free state proving that one locale-specific head is absent. */
|
|
82
|
+
export const RollbackDeleteStateSchema = Schema.Struct({
|
|
83
|
+
change: ContentDeleteSchema,
|
|
84
|
+
});
|
|
85
|
+
/** Complete full-state vocabulary used by one rollback transition. */
|
|
86
|
+
export const RollbackStateSchema = Schema.Union([
|
|
87
|
+
RollbackUpsertStateSchema,
|
|
88
|
+
RollbackDeleteStateSchema,
|
|
89
|
+
]);
|
|
90
|
+
/** Checks that current and prior states describe the same content head. */
|
|
91
|
+
function hasBoundRollbackTransition(input) {
|
|
92
|
+
return (input.current.change.contentKey === input.prior.change.contentKey &&
|
|
93
|
+
input.current.change.family === input.prior.change.family &&
|
|
94
|
+
input.current.change.artifactLocale === input.prior.change.artifactLocale);
|
|
95
|
+
}
|
|
96
|
+
/** Authenticated source state paired with the exact prior state it replaced. */
|
|
97
|
+
export const RollbackRecordSchema = Schema.Struct({
|
|
98
|
+
current: RollbackStateSchema,
|
|
99
|
+
index: ReleaseItemIndexSchema,
|
|
100
|
+
prior: RollbackStateSchema,
|
|
101
|
+
}).pipe(Schema.check(Schema.makeFilter(hasBoundRollbackTransition, {
|
|
102
|
+
message: "Expected rollback current and prior states to share one identity.",
|
|
103
|
+
})));
|
|
104
|
+
/** Indexed request for one bounded page from the exact active release. */
|
|
105
|
+
export const RollbackPageRequestSchema = Schema.Struct({
|
|
106
|
+
afterIndex: RollbackCursorSchema,
|
|
107
|
+
limit: Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween({ maximum: MAX_ROLLBACK_PAGE_RECORDS, minimum: 1 }))),
|
|
108
|
+
rollbackOf: ReleaseIdSchema,
|
|
109
|
+
rollbackOfManifestHash: Sha256HashSchema,
|
|
110
|
+
});
|
|
111
|
+
/** Checks that one response describes a real final or progressing page. */
|
|
112
|
+
function hasCoherentRollbackPage(page) {
|
|
113
|
+
const [first] = page.records;
|
|
114
|
+
const last = page.records.at(-1);
|
|
115
|
+
if (!(first && last)) {
|
|
116
|
+
return page.done && page.nextIndex === -1 && page.total === 0;
|
|
117
|
+
}
|
|
118
|
+
const hasContiguousRecords = page.records.every((record, offset) => record.index === first.index + offset);
|
|
119
|
+
return (hasContiguousRecords &&
|
|
120
|
+
last.index === page.nextIndex &&
|
|
121
|
+
page.nextIndex < page.total &&
|
|
122
|
+
page.done === (page.nextIndex + 1 === page.total));
|
|
123
|
+
}
|
|
124
|
+
/** Strict bounded response carrying current and exact prior release states. */
|
|
125
|
+
export const RollbackPageSchema = Schema.Struct({
|
|
126
|
+
done: Schema.Boolean,
|
|
127
|
+
nextIndex: RollbackCursorSchema,
|
|
128
|
+
records: Schema.Array(RollbackRecordSchema).pipe(Schema.check(Schema.isMaxLength(MAX_ROLLBACK_PAGE_RECORDS))),
|
|
129
|
+
rollbackOf: ReleaseIdSchema,
|
|
130
|
+
rollbackOfManifestHash: Sha256HashSchema,
|
|
131
|
+
total: Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
132
|
+
}).pipe(Schema.check(Schema.makeFilter(hasCoherentRollbackPage, {
|
|
133
|
+
message: "Expected one contiguous rollback page with coherent progress evidence.",
|
|
134
|
+
})));
|
|
135
|
+
/** Narrows one full rollback state to its signed prior upsert body. */
|
|
136
|
+
export function isRollbackUpsert(state) {
|
|
137
|
+
return "artifact" in state;
|
|
138
|
+
}
|
|
139
|
+
/** Serializes one complete rollback state in stable wire field order. */
|
|
140
|
+
function canonicalizeRollbackState(state) {
|
|
141
|
+
const change = JSON.stringify(canonicalizeContentChange(state.change));
|
|
142
|
+
if (!isRollbackUpsert(state)) {
|
|
143
|
+
return `{"change":${change}}`;
|
|
144
|
+
}
|
|
145
|
+
return `{"artifact":${canonicalizeSignedContentArtifact(state.artifact)},"change":${change},"projection":${canonicalizeContentProjection(state.projection)}}`;
|
|
146
|
+
}
|
|
147
|
+
/** Serializes one rollback transition in stable wire field order. */
|
|
148
|
+
export function canonicalizeRollbackRecord(record) {
|
|
149
|
+
return `{"current":${canonicalizeRollbackState(record.current)},"index":${record.index},"prior":${canonicalizeRollbackState(record.prior)}}`;
|
|
150
|
+
}
|
|
151
|
+
/** Serializes the complete body-bearing page for an exact byte ceiling. */
|
|
152
|
+
export function canonicalizeRollbackPage(page) {
|
|
153
|
+
return `{"done":${page.done},"nextIndex":${page.nextIndex},"records":[${page.records
|
|
154
|
+
.map(canonicalizeRollbackRecord)
|
|
155
|
+
.join(",")}],"rollbackOfManifestHash":${JSON.stringify(page.rollbackOfManifestHash)},"rollbackOf":${JSON.stringify(page.rollbackOf)},"total":${page.total}}`;
|
|
156
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { type ReleaseId } from "#contracts/ids";
|
|
3
|
+
import { type ContentRouteItem } from "#contracts/release/route/spec";
|
|
4
|
+
declare const RouteHashError_base: Schema.Class<RouteHashError, Schema.TaggedStruct<"RouteHashError", {
|
|
5
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
6
|
+
}>, import("effect/Cause").YieldableError>;
|
|
7
|
+
/** SHA-256 computation failed before route integrity was established. */
|
|
8
|
+
export declare class RouteHashError extends RouteHashError_base {
|
|
9
|
+
}
|
|
10
|
+
/** Keeps incremental route hash state private to one release preparation. */
|
|
11
|
+
declare class RouteDigestState {
|
|
12
|
+
#private;
|
|
13
|
+
count: number;
|
|
14
|
+
/** Initializes a domain-separated incremental route hash. */
|
|
15
|
+
constructor();
|
|
16
|
+
/** Adds one canonical route item to this invocation-owned digest. */
|
|
17
|
+
update(item: ContentRouteItem): void;
|
|
18
|
+
/** Consumes the route hash and returns its branded identity. */
|
|
19
|
+
digest(): string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
20
|
+
}
|
|
21
|
+
/** Creates fresh domain-separated route digest state. */
|
|
22
|
+
export declare function createRouteDigest(releaseId: ReleaseId): Effect.Effect<RouteDigestState, RouteHashError, never>;
|
|
23
|
+
/** Adds one canonical route item to an incremental digest. */
|
|
24
|
+
export declare function updateRouteDigest(releaseId: ReleaseId, state: RouteDigestState, item: ContentRouteItem): Effect.Effect<RouteDigestState, RouteHashError, never>;
|
|
25
|
+
/** Completes one incremental route digest with typed hash failures. */
|
|
26
|
+
export declare function completeRouteDigest(releaseId: ReleaseId, state: RouteDigestState): Effect.Effect<string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">, RouteHashError, never>;
|
|
27
|
+
/** Digests a canonical route stream without retaining its rows. */
|
|
28
|
+
export declare const digestRoutes: <E, R>(releaseId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraReleaseId">, routes: Stream.Stream<{
|
|
29
|
+
readonly change: {
|
|
30
|
+
readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
|
|
31
|
+
readonly contentKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraContentKey">;
|
|
32
|
+
readonly operation: "bind";
|
|
33
|
+
readonly publicPath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">;
|
|
34
|
+
} | {
|
|
35
|
+
readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
|
|
36
|
+
readonly operation: "delete";
|
|
37
|
+
readonly publicPath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">;
|
|
38
|
+
};
|
|
39
|
+
readonly index: number;
|
|
40
|
+
readonly releaseId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraReleaseId">;
|
|
41
|
+
}, E, R>) => Effect.Effect<{
|
|
42
|
+
count: number;
|
|
43
|
+
digest: string & import("effect/Brand").Brand<"@NakafaAI/AksaraSha256Hash">;
|
|
44
|
+
}, E | RouteHashError, R>;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Effect, Schema, Stream } from "effect";
|
|
3
|
+
import { ReleaseIdSchema, Sha256HashSchema, } from "#contracts/ids";
|
|
4
|
+
import { canonicalizeContentRouteItem, } from "#contracts/release/route/spec";
|
|
5
|
+
const CONTENT_ROUTE_DIGEST_DOMAIN = "nakafa.aksara.content-routes";
|
|
6
|
+
/** SHA-256 computation failed before route integrity was established. */
|
|
7
|
+
export class RouteHashError extends Schema.TaggedError()("RouteHashError", { releaseId: ReleaseIdSchema }) {
|
|
8
|
+
}
|
|
9
|
+
/** Keeps incremental route hash state private to one release preparation. */
|
|
10
|
+
class RouteDigestState {
|
|
11
|
+
#hash = createHash("sha256");
|
|
12
|
+
count = 0;
|
|
13
|
+
/** Initializes a domain-separated incremental route hash. */
|
|
14
|
+
constructor() {
|
|
15
|
+
this.#hash.update(CONTENT_ROUTE_DIGEST_DOMAIN);
|
|
16
|
+
this.#hash.update("\n");
|
|
17
|
+
}
|
|
18
|
+
/** Adds one canonical route item to this invocation-owned digest. */
|
|
19
|
+
update(item) {
|
|
20
|
+
this.#hash.update(canonicalizeContentRouteItem(item));
|
|
21
|
+
this.#hash.update("\n");
|
|
22
|
+
this.count += 1;
|
|
23
|
+
}
|
|
24
|
+
/** Consumes the route hash and returns its branded identity. */
|
|
25
|
+
digest() {
|
|
26
|
+
return Sha256HashSchema.make(`sha256:${this.#hash.digest("hex")}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Creates fresh domain-separated route digest state. */
|
|
30
|
+
export function createRouteDigest(releaseId) {
|
|
31
|
+
return Effect.try({
|
|
32
|
+
catch: () => new RouteHashError({ releaseId }),
|
|
33
|
+
try: () => new RouteDigestState(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Adds one canonical route item to an incremental digest. */
|
|
37
|
+
export function updateRouteDigest(releaseId, state, item) {
|
|
38
|
+
return Effect.try({
|
|
39
|
+
catch: () => new RouteHashError({ releaseId }),
|
|
40
|
+
try: () => {
|
|
41
|
+
state.update(item);
|
|
42
|
+
return state;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Completes one incremental route digest with typed hash failures. */
|
|
47
|
+
export function completeRouteDigest(releaseId, state) {
|
|
48
|
+
return Effect.try({
|
|
49
|
+
catch: () => new RouteHashError({ releaseId }),
|
|
50
|
+
try: () => state.digest(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Digests a canonical route stream without retaining its rows. */
|
|
54
|
+
export const digestRoutes = Effect.fn("AksaraContracts.digestRoutes")(function* (releaseId, routes) {
|
|
55
|
+
const initial = yield* createRouteDigest(releaseId);
|
|
56
|
+
const state = yield* routes.pipe(Stream.runFoldEffect(() => initial, (current, route) => updateRouteDigest(releaseId, current, route)));
|
|
57
|
+
const digest = yield* completeRouteDigest(releaseId, state);
|
|
58
|
+
return { count: state.count, digest };
|
|
59
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Maximum route rollback records returned by one target page. */
|
|
3
|
+
export declare const MAX_ROUTE_PAGE_RECORDS = 100;
|
|
4
|
+
/** One signed route mutation paired with its exact prior owner. */
|
|
5
|
+
export declare const RouteRollbackRecordSchema: Schema.Struct<{
|
|
6
|
+
readonly current: Schema.Struct<{
|
|
7
|
+
readonly change: Schema.Union<readonly [Schema.Struct<{
|
|
8
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
9
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
10
|
+
readonly operation: Schema.Literal<"bind">;
|
|
11
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
12
|
+
}>, Schema.Struct<{
|
|
13
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
14
|
+
readonly operation: Schema.Literal<"delete">;
|
|
15
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
16
|
+
}>]>;
|
|
17
|
+
readonly index: Schema.Finite;
|
|
18
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
19
|
+
}>;
|
|
20
|
+
readonly priorContentKey: Schema.NullOr<Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">>;
|
|
21
|
+
}>;
|
|
22
|
+
export type RouteRollbackRecord = typeof RouteRollbackRecordSchema.Type;
|
|
23
|
+
/** Indexed request for one bounded route rollback page. */
|
|
24
|
+
export declare const RoutePageRequestSchema: Schema.Struct<{
|
|
25
|
+
readonly afterIndex: Schema.Finite;
|
|
26
|
+
readonly limit: Schema.Finite;
|
|
27
|
+
readonly rollbackOf: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
28
|
+
readonly rollbackOfManifestHash: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
|
|
29
|
+
}>;
|
|
30
|
+
export type RoutePageRequest = typeof RoutePageRequestSchema.Type;
|
|
31
|
+
/** Strict bounded response carrying prior route owners for one release. */
|
|
32
|
+
export declare const RoutePageSchema: Schema.Struct<{
|
|
33
|
+
readonly done: Schema.Boolean;
|
|
34
|
+
readonly nextIndex: Schema.Finite;
|
|
35
|
+
readonly records: Schema.$Array<Schema.Struct<{
|
|
36
|
+
readonly current: Schema.Struct<{
|
|
37
|
+
readonly change: Schema.Union<readonly [Schema.Struct<{
|
|
38
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
39
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
40
|
+
readonly operation: Schema.Literal<"bind">;
|
|
41
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
42
|
+
}>, Schema.Struct<{
|
|
43
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
44
|
+
readonly operation: Schema.Literal<"delete">;
|
|
45
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
46
|
+
}>]>;
|
|
47
|
+
readonly index: Schema.Finite;
|
|
48
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
49
|
+
}>;
|
|
50
|
+
readonly priorContentKey: Schema.NullOr<Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">>;
|
|
51
|
+
}>>;
|
|
52
|
+
readonly rollbackOf: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
53
|
+
readonly rollbackOfManifestHash: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
|
|
54
|
+
readonly total: Schema.Finite;
|
|
55
|
+
}>;
|
|
56
|
+
export type RoutePage = typeof RoutePageSchema.Type;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ContentKeySchema, ReleaseIdSchema, Sha256HashSchema, } from "#contracts/ids";
|
|
3
|
+
import { ContentRouteItemSchema } from "#contracts/release/route/spec";
|
|
4
|
+
/** Maximum route rollback records returned by one target page. */
|
|
5
|
+
export const MAX_ROUTE_PAGE_RECORDS = 100;
|
|
6
|
+
const RouteCursorSchema = Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(-1)));
|
|
7
|
+
/** One signed route mutation paired with its exact prior owner. */
|
|
8
|
+
export const RouteRollbackRecordSchema = Schema.Struct({
|
|
9
|
+
current: ContentRouteItemSchema,
|
|
10
|
+
priorContentKey: Schema.NullOr(ContentKeySchema),
|
|
11
|
+
}).pipe(Schema.check(Schema.makeFilter((record) => record.current.change.operation === "delete"
|
|
12
|
+
? record.priorContentKey !== null
|
|
13
|
+
: record.priorContentKey !== record.current.change.contentKey, {
|
|
14
|
+
message: "Expected a route change to alter the prior binding owner.",
|
|
15
|
+
})));
|
|
16
|
+
/** Indexed request for one bounded route rollback page. */
|
|
17
|
+
export const RoutePageRequestSchema = Schema.Struct({
|
|
18
|
+
afterIndex: RouteCursorSchema,
|
|
19
|
+
limit: Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween({ maximum: MAX_ROUTE_PAGE_RECORDS, minimum: 1 }))),
|
|
20
|
+
rollbackOf: ReleaseIdSchema,
|
|
21
|
+
rollbackOfManifestHash: Sha256HashSchema,
|
|
22
|
+
});
|
|
23
|
+
/** Checks stable total and contiguous signed route indexes. */
|
|
24
|
+
function hasCoherentRoutePage(page) {
|
|
25
|
+
const [first] = page.records;
|
|
26
|
+
const last = page.records.at(-1);
|
|
27
|
+
if (!(first && last)) {
|
|
28
|
+
return page.done && page.nextIndex === -1 && page.total === 0;
|
|
29
|
+
}
|
|
30
|
+
const contiguous = page.records.every((record, offset) => record.current.index === first.current.index + offset);
|
|
31
|
+
return (contiguous &&
|
|
32
|
+
last.current.index === page.nextIndex &&
|
|
33
|
+
page.nextIndex < page.total &&
|
|
34
|
+
page.done === (page.nextIndex + 1 === page.total));
|
|
35
|
+
}
|
|
36
|
+
/** Strict bounded response carrying prior route owners for one release. */
|
|
37
|
+
export const RoutePageSchema = Schema.Struct({
|
|
38
|
+
done: Schema.Boolean,
|
|
39
|
+
nextIndex: RouteCursorSchema,
|
|
40
|
+
records: Schema.Array(RouteRollbackRecordSchema).pipe(Schema.check(Schema.isMaxLength(MAX_ROUTE_PAGE_RECORDS))),
|
|
41
|
+
rollbackOf: ReleaseIdSchema,
|
|
42
|
+
rollbackOfManifestHash: Sha256HashSchema,
|
|
43
|
+
total: Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
44
|
+
}).pipe(Schema.check(Schema.makeFilter(hasCoherentRoutePage, {
|
|
45
|
+
message: "Expected one contiguous route page with coherent progress evidence.",
|
|
46
|
+
})));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Signed public route change and item wire contracts. */
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
/** One public route bound to a stable locale-specific content identity. */
|
|
4
|
+
export declare const ContentRouteBindSchema: Schema.Struct<{
|
|
5
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
6
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
7
|
+
readonly operation: Schema.Literal<"bind">;
|
|
8
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
9
|
+
}>;
|
|
10
|
+
export type ContentRouteBind = typeof ContentRouteBindSchema.Type;
|
|
11
|
+
/** One public route removed without deleting its stable content identity. */
|
|
12
|
+
export declare const ContentRouteDeleteSchema: Schema.Struct<{
|
|
13
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
14
|
+
readonly operation: Schema.Literal<"delete">;
|
|
15
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
16
|
+
}>;
|
|
17
|
+
export type ContentRouteDelete = typeof ContentRouteDeleteSchema.Type;
|
|
18
|
+
/** Complete immutable public-route change vocabulary for one release. */
|
|
19
|
+
export declare const ContentRouteChangeSchema: Schema.Union<readonly [Schema.Struct<{
|
|
20
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
21
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
22
|
+
readonly operation: Schema.Literal<"bind">;
|
|
23
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
24
|
+
}>, Schema.Struct<{
|
|
25
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
26
|
+
readonly operation: Schema.Literal<"delete">;
|
|
27
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
28
|
+
}>]>;
|
|
29
|
+
export type ContentRouteChange = typeof ContentRouteChangeSchema.Type;
|
|
30
|
+
/** One ordered route change authenticated by the signed release manifest. */
|
|
31
|
+
export declare const ContentRouteItemSchema: Schema.Struct<{
|
|
32
|
+
readonly change: Schema.Union<readonly [Schema.Struct<{
|
|
33
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
34
|
+
readonly contentKey: Schema.brand<Schema.String, "@NakafaAI/AksaraContentKey">;
|
|
35
|
+
readonly operation: Schema.Literal<"bind">;
|
|
36
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
37
|
+
}>, Schema.Struct<{
|
|
38
|
+
readonly appLocale: Schema.brand<Schema.Literals<readonly ["en", "id", "de"]>, "@NakafaAI/AksaraAppLocale">;
|
|
39
|
+
readonly operation: Schema.Literal<"delete">;
|
|
40
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
41
|
+
}>]>;
|
|
42
|
+
readonly index: Schema.Finite;
|
|
43
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
44
|
+
}>;
|
|
45
|
+
export type ContentRouteItem = typeof ContentRouteItemSchema.Type;
|
|
46
|
+
/** Serializes one route change in stable wire field order. */
|
|
47
|
+
export declare function canonicalizeContentRouteChange(change: ContentRouteChange): string;
|
|
48
|
+
/** Serializes one indexed route item for signed digest computation. */
|
|
49
|
+
export declare function canonicalizeContentRouteItem(item: ContentRouteItem): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Signed public route change and item wire contracts. */
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import { ContentKeySchema, PublicPathSchema, ReleaseIdSchema, } from "#contracts/ids";
|
|
4
|
+
import { AppLocaleSchema } from "#contracts/locale";
|
|
5
|
+
import { ReleaseItemIndexSchema } from "#contracts/release/spec";
|
|
6
|
+
/** One public route bound to a stable locale-specific content identity. */
|
|
7
|
+
export const ContentRouteBindSchema = Schema.Struct({
|
|
8
|
+
appLocale: AppLocaleSchema,
|
|
9
|
+
contentKey: ContentKeySchema,
|
|
10
|
+
operation: Schema.Literal("bind"),
|
|
11
|
+
publicPath: PublicPathSchema,
|
|
12
|
+
});
|
|
13
|
+
/** One public route removed without deleting its stable content identity. */
|
|
14
|
+
export const ContentRouteDeleteSchema = Schema.Struct({
|
|
15
|
+
appLocale: AppLocaleSchema,
|
|
16
|
+
operation: Schema.Literal("delete"),
|
|
17
|
+
publicPath: PublicPathSchema,
|
|
18
|
+
});
|
|
19
|
+
/** Complete immutable public-route change vocabulary for one release. */
|
|
20
|
+
export const ContentRouteChangeSchema = Schema.Union([
|
|
21
|
+
ContentRouteBindSchema,
|
|
22
|
+
ContentRouteDeleteSchema,
|
|
23
|
+
]);
|
|
24
|
+
/** One ordered route change authenticated by the signed release manifest. */
|
|
25
|
+
export const ContentRouteItemSchema = Schema.Struct({
|
|
26
|
+
change: ContentRouteChangeSchema,
|
|
27
|
+
index: ReleaseItemIndexSchema,
|
|
28
|
+
releaseId: ReleaseIdSchema,
|
|
29
|
+
});
|
|
30
|
+
/** Serializes one route change in stable wire field order. */
|
|
31
|
+
export function canonicalizeContentRouteChange(change) {
|
|
32
|
+
if (change.operation === "delete") {
|
|
33
|
+
return `{"appLocale":${JSON.stringify(change.appLocale)},"operation":"delete","publicPath":${JSON.stringify(change.publicPath)}}`;
|
|
34
|
+
}
|
|
35
|
+
return `{"appLocale":${JSON.stringify(change.appLocale)},"contentKey":${JSON.stringify(change.contentKey)},"operation":"bind","publicPath":${JSON.stringify(change.publicPath)}}`;
|
|
36
|
+
}
|
|
37
|
+
/** Serializes one indexed route item for signed digest computation. */
|
|
38
|
+
export function canonicalizeContentRouteItem(item) {
|
|
39
|
+
return `{"change":${canonicalizeContentRouteChange(item.change)},"index":${item.index},"releaseId":${JSON.stringify(item.releaseId)}}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import type { ContentReleaseManifest } from "#contracts/release/spec";
|
|
3
|
+
declare const RouteDecodeError_base: Schema.Class<RouteDecodeError, Schema.TaggedStruct<"RouteDecodeError", {
|
|
4
|
+
readonly routeOffset: Schema.Finite;
|
|
5
|
+
}>, import("effect/Cause").YieldableError>;
|
|
6
|
+
/** One streamed route failed strict wire decoding. */
|
|
7
|
+
export declare class RouteDecodeError extends RouteDecodeError_base {
|
|
8
|
+
}
|
|
9
|
+
declare const RouteIdentityError_base: Schema.Class<RouteIdentityError, Schema.TaggedStruct<"RouteIdentityError", {
|
|
10
|
+
readonly routeOffset: Schema.Finite;
|
|
11
|
+
}>, import("effect/Cause").YieldableError>;
|
|
12
|
+
/** One route belongs to another release or sequence position. */
|
|
13
|
+
export declare class RouteIdentityError extends RouteIdentityError_base {
|
|
14
|
+
}
|
|
15
|
+
declare const RouteDuplicateError_base: Schema.Class<RouteDuplicateError, Schema.TaggedStruct<"RouteDuplicateError", {
|
|
16
|
+
readonly firstIndex: Schema.Finite;
|
|
17
|
+
readonly publicPath: Schema.brand<Schema.String, "@NakafaAI/AksaraPublicPath">;
|
|
18
|
+
}>, import("effect/Cause").YieldableError>;
|
|
19
|
+
/** Two changes in one release target the same locale-specific route. */
|
|
20
|
+
export declare class RouteDuplicateError extends RouteDuplicateError_base {
|
|
21
|
+
}
|
|
22
|
+
declare const RouteCountError_base: Schema.Class<RouteCountError, Schema.TaggedStruct<"RouteCountError", {
|
|
23
|
+
readonly actualCount: Schema.Finite;
|
|
24
|
+
readonly expectedCount: Schema.Finite;
|
|
25
|
+
}>, import("effect/Cause").YieldableError>;
|
|
26
|
+
/** The streamed route count differs from its signed manifest. */
|
|
27
|
+
export declare class RouteCountError extends RouteCountError_base {
|
|
28
|
+
}
|
|
29
|
+
declare const RouteDigestError_base: Schema.Class<RouteDigestError, Schema.TaggedStruct<"RouteDigestError", {
|
|
30
|
+
readonly actualDigest: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
|
|
31
|
+
readonly expectedDigest: Schema.brand<Schema.String, "@NakafaAI/AksaraSha256Hash">;
|
|
32
|
+
readonly releaseId: Schema.brand<Schema.String, "@NakafaAI/AksaraReleaseId">;
|
|
33
|
+
}>, import("effect/Cause").YieldableError>;
|
|
34
|
+
/** The streamed route digest differs from its signed manifest. */
|
|
35
|
+
export declare class RouteDigestError extends RouteDigestError_base {
|
|
36
|
+
}
|
|
37
|
+
/** Count authenticated without retaining complete route rows. */
|
|
38
|
+
export interface VerifiedContentRoutes {
|
|
39
|
+
readonly count: number;
|
|
40
|
+
}
|
|
41
|
+
/** Strictly decodes one replayable canonical route stream. */
|
|
42
|
+
export declare function decodeContentRoutes<E, R>(input: {
|
|
43
|
+
readonly manifest: ContentReleaseManifest;
|
|
44
|
+
readonly routes: Stream.Stream<unknown, E, R>;
|
|
45
|
+
}): Stream.Stream<{
|
|
46
|
+
readonly change: {
|
|
47
|
+
readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
|
|
48
|
+
readonly contentKey: string & import("effect/Brand").Brand<"@NakafaAI/AksaraContentKey">;
|
|
49
|
+
readonly operation: "bind";
|
|
50
|
+
readonly publicPath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">;
|
|
51
|
+
} | {
|
|
52
|
+
readonly appLocale: ("de" | "en" | "id") & import("effect/Brand").Brand<"@NakafaAI/AksaraAppLocale">;
|
|
53
|
+
readonly operation: "delete";
|
|
54
|
+
readonly publicPath: string & import("effect/Brand").Brand<"@NakafaAI/AksaraPublicPath">;
|
|
55
|
+
};
|
|
56
|
+
readonly index: number;
|
|
57
|
+
readonly releaseId: string & import("effect/Brand").Brand<"@NakafaAI/AksaraReleaseId">;
|
|
58
|
+
}, E | RouteDecodeError | RouteDuplicateError | RouteIdentityError, R>;
|
|
59
|
+
/** Authenticates a replayable route stream against its signed manifest. */
|
|
60
|
+
export declare const verifyContentRoutes: <E, R>(input: {
|
|
61
|
+
readonly manifest: ContentReleaseManifest;
|
|
62
|
+
readonly routes: Stream.Stream<unknown, E, R>;
|
|
63
|
+
}) => Effect.Effect<{
|
|
64
|
+
count: number;
|
|
65
|
+
}, E | RouteCountError | RouteDecodeError | RouteDigestError | RouteDuplicateError | import("#contracts/release/route/digest").RouteHashError | RouteIdentityError, R>;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { routeIdentity } from "#contracts/content";
|
|
3
|
+
import { PublicPathSchema, ReleaseIdSchema, Sha256HashSchema, } from "#contracts/ids";
|
|
4
|
+
import { digestRoutes } from "#contracts/release/route/digest";
|
|
5
|
+
import { ContentRouteItemSchema } from "#contracts/release/route/spec";
|
|
6
|
+
const RouteIndexSchema = Schema.Finite.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThanOrEqualTo(0)));
|
|
7
|
+
/** One streamed route failed strict wire decoding. */
|
|
8
|
+
export class RouteDecodeError extends Schema.TaggedError()("RouteDecodeError", { routeOffset: RouteIndexSchema }) {
|
|
9
|
+
}
|
|
10
|
+
/** One route belongs to another release or sequence position. */
|
|
11
|
+
export class RouteIdentityError extends Schema.TaggedError()("RouteIdentityError", { routeOffset: RouteIndexSchema }) {
|
|
12
|
+
}
|
|
13
|
+
/** Two changes in one release target the same locale-specific route. */
|
|
14
|
+
export class RouteDuplicateError extends Schema.TaggedError()("RouteDuplicateError", { firstIndex: RouteIndexSchema, publicPath: PublicPathSchema }) {
|
|
15
|
+
}
|
|
16
|
+
/** The streamed route count differs from its signed manifest. */
|
|
17
|
+
export class RouteCountError extends Schema.TaggedError()("RouteCountError", { actualCount: RouteIndexSchema, expectedCount: RouteIndexSchema }) {
|
|
18
|
+
}
|
|
19
|
+
/** The streamed route digest differs from its signed manifest. */
|
|
20
|
+
export class RouteDigestError extends Schema.TaggedError()("RouteDigestError", {
|
|
21
|
+
actualDigest: Sha256HashSchema,
|
|
22
|
+
expectedDigest: Sha256HashSchema,
|
|
23
|
+
releaseId: ReleaseIdSchema,
|
|
24
|
+
}) {
|
|
25
|
+
}
|
|
26
|
+
/** Decodes one route and applies release, index, and uniqueness invariants. */
|
|
27
|
+
function decodeRoute(manifest, state, source, routeOffset) {
|
|
28
|
+
return Schema.decodeUnknownEffect(ContentRouteItemSchema)(source, {
|
|
29
|
+
onExcessProperty: "error",
|
|
30
|
+
}).pipe(Effect.mapError(() => new RouteDecodeError({ routeOffset })), Effect.filterOrFail((item) => item.releaseId === manifest.releaseId && item.index === routeOffset, () => new RouteIdentityError({ routeOffset })), Effect.flatMap((item) => {
|
|
31
|
+
const identity = routeIdentity(item.change);
|
|
32
|
+
const firstIndex = state.firstIndex.get(identity);
|
|
33
|
+
if (firstIndex !== undefined) {
|
|
34
|
+
return Effect.fail(new RouteDuplicateError({
|
|
35
|
+
firstIndex,
|
|
36
|
+
publicPath: item.change.publicPath,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
state.firstIndex.set(identity, item.index);
|
|
40
|
+
return Effect.succeed(item);
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/** Strictly decodes one replayable canonical route stream. */
|
|
44
|
+
export function decodeContentRoutes(input) {
|
|
45
|
+
return Stream.unwrap(Effect.sync(() => {
|
|
46
|
+
const state = { firstIndex: new Map() };
|
|
47
|
+
return input.routes.pipe(Stream.zipWithIndex, Stream.mapEffect(([source, routeOffset]) => decodeRoute(input.manifest, state, source, routeOffset)));
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/** Authenticates a replayable route stream against its signed manifest. */
|
|
51
|
+
export const verifyContentRoutes = Effect.fn("AksaraContracts.verifyContentRoutes")(function* (input) {
|
|
52
|
+
const summary = yield* digestRoutes(input.manifest.releaseId, decodeContentRoutes(input));
|
|
53
|
+
if (summary.count !== input.manifest.routeCount) {
|
|
54
|
+
return yield* new RouteCountError({
|
|
55
|
+
actualCount: summary.count,
|
|
56
|
+
expectedCount: input.manifest.routeCount,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (summary.digest !== input.manifest.routeDigest) {
|
|
60
|
+
return yield* new RouteDigestError({
|
|
61
|
+
actualDigest: summary.digest,
|
|
62
|
+
expectedDigest: input.manifest.routeDigest,
|
|
63
|
+
releaseId: input.manifest.releaseId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return { count: summary.count };
|
|
67
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Sha256HashSchema } from "#contracts/ids";
|
|
2
|
+
import type { ContentReleaseManifest } from "#contracts/release/spec";
|
|
3
|
+
/** Produces stable manifest bytes including locale and source identity. */
|
|
4
|
+
export declare function canonicalizeContentReleaseManifest(manifest: ContentReleaseManifest): string;
|
|
5
|
+
/** Returns domain-separated canonical bytes covered by release Ed25519. */
|
|
6
|
+
export declare function canonicalizeContentReleaseSigningInput(manifestHash: typeof Sha256HashSchema.Type, manifest: ContentReleaseManifest): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { canonicalizeReleaseOrigin } from "#contracts/release/origin";
|
|
2
|
+
import { canonicalizePublicationScope } from "#contracts/release/snapshot/scope";
|
|
3
|
+
import { canonicalizeContentSnapshotSet } from "#contracts/release/snapshot/spec";
|
|
4
|
+
const CONTENT_RELEASE_SIGNATURE_DOMAIN = "nakafa.aksara.localized-content-release";
|
|
5
|
+
/** Produces stable manifest bytes including locale and source identity. */
|
|
6
|
+
export function canonicalizeContentReleaseManifest(manifest) {
|
|
7
|
+
return JSON.stringify({
|
|
8
|
+
activeAppLocales: manifest.activeAppLocales,
|
|
9
|
+
baseActiveAppLocales: manifest.baseActiveAppLocales,
|
|
10
|
+
baseManifestHash: manifest.baseManifestHash,
|
|
11
|
+
baseReleaseId: manifest.baseReleaseId,
|
|
12
|
+
baseResultCount: manifest.baseResultCount,
|
|
13
|
+
baseResultDigest: manifest.baseResultDigest,
|
|
14
|
+
deleteCount: manifest.deleteCount,
|
|
15
|
+
format: manifest.format,
|
|
16
|
+
itemCount: manifest.itemCount,
|
|
17
|
+
itemsDigest: manifest.itemsDigest,
|
|
18
|
+
origin: canonicalizeReleaseOrigin(manifest.origin),
|
|
19
|
+
projectionCount: manifest.projectionCount,
|
|
20
|
+
projectionDigest: manifest.projectionDigest,
|
|
21
|
+
releaseId: manifest.releaseId,
|
|
22
|
+
rendererContractVersion: manifest.rendererContractVersion,
|
|
23
|
+
rendererManifestHash: manifest.rendererManifestHash,
|
|
24
|
+
resultCount: manifest.resultCount,
|
|
25
|
+
resultDigest: manifest.resultDigest,
|
|
26
|
+
rollbackCount: manifest.rollbackCount,
|
|
27
|
+
rollbackDigest: manifest.rollbackDigest,
|
|
28
|
+
routeCount: manifest.routeCount,
|
|
29
|
+
routeDigest: manifest.routeDigest,
|
|
30
|
+
scope: canonicalizePublicationScope(manifest.scope),
|
|
31
|
+
snapshots: canonicalizeContentSnapshotSet(manifest.snapshots),
|
|
32
|
+
upsertCount: manifest.upsertCount,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Returns domain-separated canonical bytes covered by release Ed25519. */
|
|
36
|
+
export function canonicalizeContentReleaseSigningInput(manifestHash, manifest) {
|
|
37
|
+
return `${CONTENT_RELEASE_SIGNATURE_DOMAIN}\n${manifestHash}\n${canonicalizeContentReleaseManifest(manifest)}`;
|
|
38
|
+
}
|