Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
672800a
test(reliability): define durable ETag resource-boundary RED
seonghobae Aug 11, 2026
e699def
fix(reliability): bound durable ETag validation
seonghobae Aug 11, 2026
f7bb50d
test(reliability): cover durable ETag retention boundaries
seonghobae Aug 11, 2026
abf9e23
docs(index): expose provider-neutral autosave contract
seonghobae Aug 11, 2026
02d1138
test(reliability): prove exact durable ETag ceiling semantics
seonghobae Aug 11, 2026
aceea53
docs(doctoring): record durable ETag resource boundary
seonghobae Aug 11, 2026
b957779
test(package): verify autosave ETag resource ceiling
seonghobae Aug 11, 2026
b356eb9
docs(index): distinguish active autosave resource proposal
seonghobae Aug 11, 2026
f5e1fa4
test(reliability): prove oversized ETags never reach observers
seonghobae Aug 11, 2026
3262a1a
test(docs): enforce active ETag resource-boundary maturity
seonghobae Aug 11, 2026
d481346
test(reliability): prove oversized ETag failures stay redacted
seonghobae Aug 11, 2026
a063212
test(reliability): preflight malformed durable save status
seonghobae Aug 15, 2026
dba2d9f
fix(reliability): preflight durable save status
seonghobae Aug 15, 2026
8fa10e8
test(autosave): reject hidden durable result fields
seonghobae Aug 15, 2026
ffe87e8
fix(autosave): reject hidden durable result fields
seonghobae Aug 15, 2026
79cd18d
test(autosave): preflight saved validator before own-key scan
seonghobae Aug 15, 2026
5cbbaac
fix(autosave): preflight saved validator before own-key scan
seonghobae Aug 15, 2026
9b5ade5
test(autosave): cover exact saved-result shape rejection
seonghobae Aug 15, 2026
edfd32c
fix(ci): reconcile release workflow with protected main
seonghobae Aug 16, 2026
c72e925
fix(ci): align release asset contract tests
seonghobae Aug 16, 2026
71d1bf2
docs(release): converge four-file SBOM contract
seonghobae Aug 16, 2026
2bd9b91
chore: synchronize durable autosave boundary with protected main
seonghobae Aug 17, 2026
0e0694a
fix(release): preserve protected exact-checkout workflow on autosave …
seonghobae Aug 18, 2026
159b927
Merge remote-tracking branch 'origin/main' into codex/pr188-restack
seonghobae Sep 4, 2026
18b7ff4
test(ci): cover event-specific Python matrix
seonghobae Sep 4, 2026
3dd5838
test(ci): bind Python matrix to event
seonghobae Sep 4, 2026
c70c92e
revert(ci): restore Office contract owner
seonghobae Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This directory is the discoverable index for Inkspan's product, technical, secur
| [`PRD.md`](PRD.md) | Product users, jobs, buyer outcomes, non-goals, acceptance and claim boundaries |
| [`TRD.md`](TRD.md) | Technical invariants, runtime boundaries, failure semantics and release evidence |
| [`CONTRACTS.md`](CONTRACTS.md) | Public package/API/event/schema/plugin/collaboration and host-integration contracts |
| [`document-autosave.md`](document-autosave.md) | Provider-neutral local autosave ordering, durable-validator handoff, recovery, lifecycle observation, and host-owned persistence boundary |
| [`package-distribution.md`](package-distribution.md) | Buyer-facing public npm package entrypoints, packaged contents, runtime dependency boundaries, and consumer verification |
| [`email-output.md`](email-output.md) | Deterministic email fragment/full-document authority, language/direction metadata, accessibility and host-owned transport boundary |
| [`print-output.md`](print-output.md) | Browser print/paged-media presentation, accessibility/fidelity limits, host-owned governed-export boundary, and rollback |
Expand All @@ -32,6 +33,10 @@ Root `SECURITY.md` is now implemented on protected `main` and remains the normat

Dated reassessments capture a reviewed source generation and its active or operational deltas without placing mutable workflow-run identities into timeless architecture. They do not override protected `main`, accepted ADRs, or current exact-head evidence.

## Active implementation records

Active-PR doctoring must remain explicitly non-shipped and must not replace the protected-main authority above. The current autosave reliability proposal is recorded in [`doctoring/durable-etag-resource-boundary.md`](doctoring/durable-etag-resource-boundary.md); its 64 Ki local validator ceiling is **Active PR / Proposed** until integrated into protected `main` with required exact-head evidence.

## Status discipline

Use these terms consistently:
Expand Down
59 changes: 59 additions & 0 deletions docs/doctoring/durable-etag-resource-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Doctoring record: Durable ETag resource boundary

**Date:** 2026-08-11
**Status:** Active PR / Proposed
**Protected-main authority:** The current protected implementation validates RFC 9110 strong entity-tag syntax but does not yet apply the resource ceiling described below.
**Scope:** Provider-neutral durable autosave validator validation only.

## Buyer-visible gap

Inkspan's durable autosave session accepts server-issued strong entity tags at three local trust boundaries: initial session creation, successful durable-save callback results, and explicit conflict/failure recovery. Protected `main` validates the RFC 9110 character grammar with a regular expression, but the validator has no Inkspan-owned input ceiling. An arbitrarily large syntactically valid string can therefore force an unbounded regex scan before classification and, when accepted, become retained session/snapshot metadata.

The host still owns transport and server field-size policy. Inkspan nevertheless owns the local validator predicate and retained local session state, so it must place a bounded resource policy before its own parser/regex boundary.

## Decision

The active implementation proposal caps the complete quoted strong entity tag at **64 Ki UTF-16 code units**. Values above that ceiling fail closed before the RFC grammar regular expression is evaluated. Values at or below the ceiling still have to satisfy the existing strong entity-tag grammar.

This 64 Ki ceiling is an Inkspan local reliability/resource policy. RFC 9110 defines entity-tag syntax and comparison semantics; this record does **not** claim that RFC 9110 defines a 64 Ki entity-tag or HTTP-field maximum.

One public predicate, `isStrongHttpEntityTag()`, remains the validation authority. Using the same predicate for initial options, returned replacement validators, and recovered validators prevents those entry points from drifting to different size or grammar rules.

## Alternatives considered

1. **Keep grammar-only validation.** Rejected because the local regex and retained snapshot state remain attacker/caller-amplifiable.
2. **Apply a host-configurable limit.** Rejected for the standalone predicate because every caller would need to re-establish a safe default, weakening deterministic package behavior. Hosts with different version-token protocols already have the lower-level autosave queue escape hatch.
3. **Use an HTTP transport/server limit as the only bound.** Rejected because standalone Inkspan has no transport authority and callers can invoke the public validator directly.
4. **Use UTF-8 byte counting.** Not selected for this boundary because the accepted HTTP `etagc` grammar is already restricted to ASCII plus `obs-text`; a constant-time JavaScript string-length preflight is sufficient to prevent the regex scan and directly bounds retained JS string size in code units. This does not change any host transport byte limit.

## Failure and privacy semantics

Oversized initial validators continue to surface only the redacted `invalid_options` category. Oversized recovery validators surface only `invalid_recovery_validator`. Oversized callback replacement validators are treated as an invalid save result and leave the previously accepted durable validator intact. None of those public failures copy the rejected validator into the error message.

The accepted durable validator remains tenant-correlatable metadata. Existing guidance prohibiting public URLs, unauthenticated logs, analytics dimensions, and high-cardinality metric labels remains unchanged.

## Ownership boundary

Inkspan owns the resource-bounded local predicate and deterministic local validator handoff. The host continues to own authentication, authorization, tenancy, network transport, HTTP server configuration, persistence, atomic `If-Match` comparison/commit, credentials, migration, retention, durable audit, retry/idempotency policy, and conflict UX. No network, database, model, credential, or durable PDF/print authority is added.

## Verification contract

The change is accepted only when exact-head evidence proves all of the following:

- a test-only predecessor fails because an oversized otherwise-valid tag reaches the old grammar-only path;
- the repaired predicate rejects oversized input before regex evaluation;
- ASCII and `obs-text` values at the exact local ceiling remain accepted when syntactically valid;
- the first otherwise-valid code unit beyond the ceiling is rejected;
- initial-session, replacement-result, and recovery boundaries all fail closed without retaining the oversized value;
- public errors remain payload-redacted;
- the framework-free autosave package surface preserves the same behavior;
- owned production statement, branch, function, and line coverage remains exactly 100%; and
- applicable CI, security, package, browser, Office, provenance/release-policy and review gates pass on one unchanged exact head.

Until that active PR reaches protected `main`, the behavior in this record is proposed and must not be described as shipped.

## References (APA 7th edition)

Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). RFC Editor. https://doi.org/10.17487/RFC9110

International Organization for Standardization. (2023). *Systems and software engineering—Systems and software quality requirements and evaluation (SQuaRE)—Product quality model* (ISO/IEC 25010:2023). https://www.iso.org/standard/78176.html
14 changes: 14 additions & 0 deletions src/autosave/session.entityTagPackageBoundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { isStrongHttpEntityTag } from './package.js';

const MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS = 64 * 1024;

describe('framework-free autosave entity-tag package boundary', () => {
it('exposes the same fail-closed ceiling through the standalone package barrel', () => {
const atCeiling = `"${'p'.repeat(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS - 2)}"`;
const beyondCeiling = `"${'p'.repeat(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS - 1)}"`;

expect(isStrongHttpEntityTag(atCeiling)).toBe(true);
expect(isStrongHttpEntityTag(beyondCeiling)).toBe(false);
});
});
239 changes: 239 additions & 0 deletions src/autosave/session.entityTagResourceBoundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { describe, expect, it, vi } from 'vitest';
import {
createDocumentAutosaveSession,
isStrongHttpEntityTag,
type DocumentAutosaveRevisionEvidence,
type DocumentAutosaveSessionSnapshot,
} from './package.js';

const MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS = 64 * 1024;
const PRIVATE_ETAG_MARKER = 'PRIVATE_ETAG_SENTINEL';

/** Create one exact immutable revision fixture for durable-validator tests. */
function createEvidence(): DocumentAutosaveRevisionEvidence {
const digestHex = '41'.repeat(32);
return Object.freeze({
envelope: Object.freeze({
schemaId: 'https://inkspan.io/schemas/document-envelope/v1',
schemaVersion: 1,
documentJson: Object.freeze({ type: 'doc' }),
}),
revision: Object.freeze({
algorithm: 'SHA-256',
digestHex,
strongEntityTag: `"sha256-${digestHex}"`,
}),
});
}

/** Create one syntactically RFC-compatible tag beyond Inkspan's local ceiling. */
function createOversizedEntityTag(): string {
return `"${PRIVATE_ETAG_MARKER}${'a'.repeat(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS)}"`;
}

describe('durable autosave entity-tag resource boundary', () => {
it('rejects an obviously oversized validator before regex evaluation', () => {
const regexTest = vi.spyOn(RegExp.prototype, 'test');

expect(isStrongHttpEntityTag(createOversizedEntityTag())).toBe(false);
expect(regexTest).not.toHaveBeenCalled();

regexTest.mockRestore();
});

it('preserves syntactically valid ASCII and obs-text validators at the exact ceiling', () => {
const exactAsciiCandidate = `"${'a'.repeat(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS - 2)}"`;
const exactObsTextCandidate = `"${String.fromCharCode(0xff).repeat(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS - 2)}"`;

expect(exactAsciiCandidate).toHaveLength(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS);
expect(exactObsTextCandidate).toHaveLength(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS);
expect(isStrongHttpEntityTag(exactAsciiCandidate)).toBe(true);
expect(isStrongHttpEntityTag(exactObsTextCandidate)).toBe(true);
});

it('rejects the first otherwise-valid code unit beyond the complete-tag ceiling', () => {
const oneOverCandidate = `"${'a'.repeat(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS - 1)}"`;

expect(oneOverCandidate).toHaveLength(MAX_ACCEPTED_ENTITY_TAG_CODE_UNITS + 1);
expect(isStrongHttpEntityTag(oneOverCandidate)).toBe(false);
});

it('rejects an oversized initial validator with a payload-redacted error', () => {
let capturedError: unknown;
try {
createDocumentAutosaveSession({
initialStrongEntityTag: createOversizedEntityTag(),
save: () => ({ status: 'conflict' }),
});
} catch (error) {
capturedError = error;
}

expect(capturedError).toMatchObject({ code: 'invalid_options' });
expect((capturedError as Error).message).not.toContain(PRIVATE_ETAG_MARKER);
});

it('rejects an oversized recovered validator without replacing or exposing it', async () => {
const session = createDocumentAutosaveSession({
initialStrongEntityTag: '"server-one"',
save: () => ({ status: 'conflict' }),
});

await expect(session.enqueue(createEvidence())).resolves.toMatchObject({
status: 'conflict',
});
let capturedError: unknown;
try {
session.resume(createOversizedEntityTag());
} catch (error) {
capturedError = error;
}
expect(capturedError).toMatchObject({ code: 'invalid_recovery_validator' });
expect((capturedError as Error).message).not.toContain(PRIVATE_ETAG_MARKER);
expect(session.getSnapshot()).toMatchObject({
state: 'blocked',
durableStrongEntityTag: '"server-one"',
});
});

it('fails closed without emitting or exposing an oversized replacement validator', async () => {
const oversizedEntityTag = createOversizedEntityTag();
const observedSnapshots: DocumentAutosaveSessionSnapshot[] = [];
const session = createDocumentAutosaveSession({
initialStrongEntityTag: '"server-one"',
save: () => ({
status: 'saved',
nextStrongEntityTag: oversizedEntityTag,
}),
onSnapshotChange(snapshot) {
observedSnapshots.push(snapshot);
},
});

const capturedError = await session.enqueue(createEvidence()).then(
() => null,
(error: unknown) => error,
);
expect(capturedError).toMatchObject({ code: 'invalid_save_result' });
expect((capturedError as Error).message).not.toContain(PRIVATE_ETAG_MARKER);
await Promise.resolve();
expect(session.getSnapshot()).toMatchObject({
state: 'blocked',
blockedReason: 'failure',
durableStrongEntityTag: '"server-one"',
});
expect(observedSnapshots.length).toBeGreaterThan(0);
expect(
observedSnapshots.every(
(snapshot) => snapshot.durableStrongEntityTag === '"server-one"',
),
).toBe(true);
expect(JSON.stringify(observedSnapshots)).not.toContain(PRIVATE_ETAG_MARKER);
});

it('rejects malformed save status before enumerating caller-owned keys', async () => {
let ownKeysCalls = 0;
const invalidResult = new Proxy(
{ status: 'invalid' },
{
ownKeys() {
ownKeysCalls += 1;
throw new Error(PRIVATE_ETAG_MARKER);
},
},
);
const session = createDocumentAutosaveSession({
initialStrongEntityTag: '"server-one"',
save: () => invalidResult as never,
});

const capturedError = await session.enqueue(createEvidence()).then(
() => null,
(error: unknown) => error,
);

expect(capturedError).toMatchObject({ code: 'invalid_save_result' });
expect((capturedError as Error).message).not.toContain(PRIVATE_ETAG_MARKER);
expect(ownKeysCalls).toBe(0);
});

it('rejects malformed saved results before enumerating caller-owned keys', async () => {
let ownKeysCalls = 0;
const invalidResult = new Proxy(
{ status: 'saved' },
{
ownKeys() {
ownKeysCalls += 1;
throw new Error(PRIVATE_ETAG_MARKER);
},
},
);
const session = createDocumentAutosaveSession({
initialStrongEntityTag: '"server-one"',
save: () => invalidResult as never,
});

const capturedError = await session.enqueue(createEvidence()).then(
() => null,
(error: unknown) => error,
);

expect(capturedError).toMatchObject({ code: 'invalid_save_result' });
expect((capturedError as Error).message).not.toContain(PRIVATE_ETAG_MARKER);
expect(ownKeysCalls).toBe(0);
});

it('rejects saved durable results with extra enumerable fields', async () => {
const session = createDocumentAutosaveSession({
initialStrongEntityTag: '"server-one"',
save: () => ({
status: 'saved',
nextStrongEntityTag: '"server-two"',
unexpected: true,
}) as never,
});

const capturedError = await session.enqueue(createEvidence()).then(
() => null,
(error: unknown) => error,
);

expect(capturedError).toMatchObject({ code: 'invalid_save_result' });
expect(session.getSnapshot()).toMatchObject({
state: 'blocked',
durableStrongEntityTag: '"server-one"',
});
});

it('rejects non-enumerable durable result fields as non-contract objects', async () => {
const hiddenConflict = Object.defineProperty({}, 'status', {
value: 'conflict',
enumerable: false,
});
const hiddenValidator = Object.defineProperties(
{},
{
status: { value: 'saved', enumerable: true },
nextStrongEntityTag: { value: '"server-two"', enumerable: false },
},
);

for (const invalidResult of [hiddenConflict, hiddenValidator]) {
const session = createDocumentAutosaveSession({
initialStrongEntityTag: '"server-one"',
save: () => invalidResult as never,
});

const capturedError = await session.enqueue(createEvidence()).then(
() => null,
(error: unknown) => error,
);

expect(capturedError).toMatchObject({ code: 'invalid_save_result' });
expect(session.getSnapshot()).toMatchObject({
state: 'blocked',
durableStrongEntityTag: '"server-one"',
});
}
});
});
Loading
Loading