First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:03 -04:00
co-authored by Claude Opus 5.5
commit d44e69a207
14 changed files with 2235 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
*.log
.DS_Store
.env
.env.*
!.env.example
.npmrc
dist-cjs/
+6
View File
@@ -0,0 +1,6 @@
stages: [test]
test:
stage: test
image: node:22-alpine
script: [npm ci, npm run build, npm test]
+16
View File
@@ -0,0 +1,16 @@
# Changelog
## 0.1.0 - 2026-09-28
First public release under MIT.
- `keyVerdict()` checks a signing key at both origin and arrival;
`expired-in-flight` is its own verdict, accepted and recorded.
- Revocation applies regardless of origin time.
- `assessStream()` returns `apply` in sender-sequence order, never arrival
order.
- A duplicate fails when the receiver is not idempotent and is accepted when
it is.
- A message beyond `maxTransitSeconds` is rejected for action.
- A message received before it was sent is NOT ASSESSED.
- `now` is never read: the same trace gives the same verdicts.
+21
View File
@@ -0,0 +1,21 @@
# Contributing
Issues and merge requests are welcome at
https://gitlab.com/extant2000/slow-lane.
## Ground rules
- A test that cannot fail proves nothing. If you fix a bug, add a test and
check that it fails against the unfixed code before you submit.
- Measure, don't assume. Two modules with the same line count can still be
different programs.
- Keep dependencies minimal. Every new runtime dependency needs a reason.
- Explain why in comments, not what. The what is already in the code.
- A breaking change needs a major version bump and a note in CHANGELOG.md.
## Before you open a merge request
Run both of these and make sure they pass:
npm run build
npm test
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Extant 2000 LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+96
View File
@@ -0,0 +1,96 @@
# SlowLane
Check signatures, ordering and duplicates on a link that is slow,
intermittent and out of order.
SlowLane assesses a batch of received messages and tells you which ones to
apply, in which order, and which to reject. It is pure, with no clock and no
network. `now` is never read: every decision comes from the timestamps
carried on the messages, so the same trace always produces the same verdicts.
## What it checks and why
Security checks written for a fast, ordered, exactly-once link break on a
link that is none of those things. Worse, they fail in a direction that looks
correct. Three cases:
1. Expiry checked at the wrong end of the flight. A message signed with a key
that was valid when it was sent can arrive after the key expired. Checking
the key at arrival rejects legitimate traffic, and on a link with hours of
transit that is most of it. Checking only at origin accepts a message
whose key is no longer trusted. Each check is wrong on its own, so
`keyVerdict()` checks both ends and returns `expired-in-flight` as its own
verdict. Such a message is accepted and recorded. Revocation is different
and ignores origin time entirely, because a key is usually compromised
before anyone notices.
2. Ordering by arrival. Arrival order is a property of the link, not of the
sender. Applying updates in receipt order overwrites newer state with
older and looks like it worked. The per-sender `sequence` is the only
ordering authority, and `apply` comes back sorted by it. A caller that
loops over the input array instead has brought the bug back.
3. At-least-once delivery meeting a handler that is not idempotent. On this
kind of link a duplicate is the delivery guarantee working. It is a
finding only when the receiver has not declared itself idempotent. The
duplicate is not the anomaly; the assumption of exactly-once is.
Also:
- Too old to act on. Past `maxTransitSeconds`, a message is rejected but
worth recording. The world it described has moved on.
- Received before sent. NOT ASSESSED. The clocks disagree.
- No recognised key. NOT ASSESSED and rejected, since nothing establishes who
sent it.
## Usage
```ts
import { assessStream, formatStream, keyVerdict, type Message } from '@extant2000/slow-lane'
const messages: Message[] = [
{ id: 'm3', sender: 'probe-a', sequence: 3, originAt: '2026-09-28T10:05:00Z', receivedAt: '2026-09-28T10:25:00Z', keyId: 'k1', where: 'downlink.log' },
{ id: 'm1', sender: 'probe-a', sequence: 1, originAt: '2026-09-28T09:00:00Z', receivedAt: '2026-09-28T10:30:00Z', keyId: 'k1', where: 'downlink.log' },
{ id: 'm2', sender: 'probe-a', sequence: 2, originAt: '2026-09-28T10:00:00Z', receivedAt: '2026-09-28T10:20:00Z', keyId: 'k1', where: 'downlink.log' },
{ id: 'm2b', sender: 'probe-a', sequence: 2, originAt: '2026-09-28T10:00:00Z', receivedAt: '2026-09-28T10:40:00Z', keyId: 'k1', where: 'downlink.log' },
]
const { apply, reject, report } = assessStream(messages, {
keys: [{ id: 'k1', notAfter: '2026-09-28T10:10:00Z' }],
maxTransitSeconds: 3600,
receiverIsIdempotent: false,
})
for (const m of apply) {
// sender order, not arrival order
}
```
`report` is an evidence-record `CapabilityReport`. Print it with
`formatStream()`.
## Sample output
Produced by running the example above and printing `apply`, `reject` and
each finding's determination and summary:
```
apply: m2, m3
reject: m1, m2b
not-applicable probe-a#1 was signed with a key that expired while in flight
fail probe-a#1 spent 90 minutes in transit
not-applicable probe-a#2 was signed with a key that expired while in flight
pass probe-a#2 accepted, 1200s in transit
not-applicable probe-a#2 was signed with a key that expired while in flight
fail probe-a#2 was delivered more than once
not-applicable probe-a#3 was signed with a key that expired while in flight
pass probe-a#3 accepted, 1200s in transit
```
## Install
```
npm install @extant2000/slow-lane
```
## License
MIT. Copyright (c) 2026 Extant 2000 LLC. See [LICENSE](LICENSE).
+11
View File
@@ -0,0 +1,11 @@
# Security policy
## Reporting a vulnerability
Please report vulnerabilities privately by email to security@extant2000.com.
Do not open a public issue or merge request for a security problem.
Include the affected version, a description of the issue, and steps or a test
that reproduce it if you have them.
We aim to acknowledge every report within 5 business days.
+1504
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@extant2000/slow-lane",
"version": "0.1.0",
"private": false,
"description": "Authenticated store-and-forward for minutes-long links: replay-resistant and reorder-tolerant.",
"license": "MIT",
"type": "module",
"main": "./dist-cjs/index.js",
"files": [
"dist",
"dist-cjs",
"src",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "https://gitlab.com/extant2000/slow-lane.git"
},
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist-cjs/index.js"
}
},
"scripts": {
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"typescript": "^5.6.0",
"vitest": "^2.1.0"
},
"dependencies": {
"@extant2000/evidence-record": "^0.1.2"
},
"author": "Extant 2000 LLC",
"homepage": "https://gitlab.com/extant2000/slow-lane",
"bugs": {
"url": "https://gitlab.com/extant2000/slow-lane/-/issues"
}
}
+322
View File
@@ -0,0 +1,322 @@
/**
* SlowLane — secure delayed, intermittent and out-of-order communications.
*
* Every security primitive that assumes a fast, ordered, exactly-once link
* breaks on one that is none of those things, and it breaks in a direction
* that looks correct:
*
* 1. **Expiry checked at the wrong end of the flight.** A message signed
* with a key valid at transmission can arrive after that key expired.
* Validating against arrival time rejects legitimate traffic, and on a
* link with hours of transit that is most of it. Validating against
* origin time alone silently accepts a message whose key is now
* revoked. Both are wrong on their own, so both are computed and the
* in-flight expiry is reported SEPARATELY rather than folded into the
* verdict.
*
* 2. **Ordering by arrival.** Arrival order is a property of the link, not
* of the sender. Applying updates in receipt order silently overwrites
* a newer state with an older one — and it looks like it worked. The
* per-sender sequence is the only ordering authority here.
*
* 3. **At-least-once delivery meeting a non-idempotent handler.** A
* duplicate is not an anomaly on this kind of link; it is the delivery
* guarantee working. A duplicate is a finding only when the receiver
* has not declared itself idempotent.
*
* Pure: no clock, no network. `now` is never read — every decision is made
* from timestamps carried on the messages themselves, so the same trace
* always produces the same verdicts.
*/
import {
type CapabilityReport,
type Finding,
type FormatOptions,
evidence,
formatReport as renderReport,
} from '@extant2000/evidence-record'
export interface SigningKey {
id: string
/** ISO 8601. The key is invalid for messages ORIGINATED after this. */
notAfter?: string
/** ISO 8601. Revoked outright — invalid regardless of origin time. */
revokedAt?: string
}
export interface Message {
id: string
sender: string
/**
* Monotonic per-sender sequence number.
*
* The only ordering authority. Arrival order is a property of the link.
*/
sequence: number
/** ISO 8601, stamped by the sender. */
originAt: string
/** ISO 8601, stamped by the receiver. */
receivedAt: string
/** Key the message was signed with. */
keyId?: string
/** True when applying this message twice is harmless. */
idempotent?: boolean
/** Where the message was observed — a log, a queue, a downlink. */
where: string
}
export interface LinkPolicy {
/**
* Beyond this transit time a message is too old to ACT on, though it may
* still be worth recording. Absent means no limit is declared.
*/
maxTransitSeconds?: number
keys?: SigningKey[]
/** True when the receiving handler is idempotent for every message. */
receiverIsIdempotent?: boolean
}
const secondsBetween = (a: string, b: string): number | null => {
const x = Date.parse(a); const y = Date.parse(b)
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
return Math.round((y - x) / 1000)
}
export interface TransitStats {
/** Seconds in flight, `null` when either timestamp is unparseable. */
seconds: number | null
/** True when the receiver stamped it as arriving before it was sent. */
impossible: boolean
}
/** How long a message was in flight, and whether that is even coherent. */
export function transit(m: Message): TransitStats {
const s = secondsBetween(m.originAt, m.receivedAt)
return { seconds: s, impossible: s !== null && s < 0 }
}
export type KeyVerdict =
/** Valid at origin and still valid now. */
| 'valid'
/** Valid when sent, expired while in flight. Normal on this link. */
| 'expired-in-flight'
/** Not valid even at origin. */
| 'invalid-at-origin'
/** Revoked outright — origin time does not rescue it. */
| 'revoked'
/** No key, or the key is not in the declared set. */
| 'unknown'
/**
* Judge a signature key against BOTH ends of the flight.
*
* `expired-in-flight` is the case a single check cannot express. Validating
* at arrival calls it invalid and drops legitimate traffic; validating at
* origin calls it fine and hides that the key is no longer trusted. It is
* neither, so it gets its own answer.
*/
export function keyVerdict(m: Message, policy: LinkPolicy): KeyVerdict {
if (!m.keyId) return 'unknown'
const key = (policy.keys ?? []).find(k => k.id === m.keyId)
if (!key) return 'unknown'
// Revocation is not a schedule. It applies regardless of when the message
// was sent, because the reason for revoking is usually that the key was
// compromised before anyone noticed.
if (key.revokedAt) return 'revoked'
if (!key.notAfter) return 'valid'
const originOk = Date.parse(m.originAt) <= Date.parse(key.notAfter)
const arrivalOk = Date.parse(m.receivedAt) <= Date.parse(key.notAfter)
if (!originOk) return 'invalid-at-origin'
return arrivalOk ? 'valid' : 'expired-in-flight'
}
function msgEvidence(m: Message) {
return evidence.file(m.where, undefined, `${m.sender}#${m.sequence}`,
`sent ${m.originAt}, received ${m.receivedAt}`)
}
export interface StreamAssessment {
report: CapabilityReport
/** Messages that must not be applied, worst first. */
reject: Message[]
/** Messages safe to apply, in SENDER order, not arrival order. */
apply: Message[]
}
/**
* Assess a received stream.
*
* `apply` comes back sorted by sender sequence, not by arrival — the whole
* point. A caller that iterates the input array instead has reintroduced
* the bug this library exists to prevent.
*/
export function assessStream(messages: Message[], policy: LinkPolicy = {}): StreamAssessment {
const list = [...(messages ?? [])]
if (list.length === 0) {
return {
report: {
capability: 'SlowLane',
scope: 'no messages',
examined: 0,
findings: [],
notes: ['No messages were supplied. On an intermittent link, silence is not evidence that nothing was sent.'],
},
reject: [],
apply: [],
}
}
const findings: Finding[] = []
const reject: Message[] = []
const apply: Message[] = []
// Highest sequence already seen per sender, in SENDER order.
const highest = new Map<string, number>()
const seen = new Set<string>()
const bySequence = [...list].sort(
(a, b) => a.sender.localeCompare(b.sender) || a.sequence - b.sequence,
)
for (const m of bySequence) {
const key = `${m.sender}#${m.sequence}`
const t = transit(m)
const verdict = keyVerdict(m, policy)
let rejected = false
if (t.impossible) {
findings.push({
id: `${m.id}/clock`,
summary: `${key} was received before it was sent`,
determination: 'not-assessed',
severity: 'high',
detail: 'The two clocks disagree, so neither transit time nor any expiry decision derived from them can be trusted for this message.',
evidence: [msgEvidence(m)],
})
reject.push(m)
continue
}
if (verdict === 'revoked' || verdict === 'invalid-at-origin') {
findings.push({
id: `${m.id}/key`,
summary: `${key} signed with a ${verdict === 'revoked' ? 'revoked' : 'not-yet-or-no-longer-valid'} key`,
determination: 'fail',
severity: 'critical',
detail: verdict === 'revoked'
? 'Revocation is not a schedule — it applies regardless of when the message was sent, because the key was usually compromised before anyone noticed.'
: 'The key was already invalid when the message was created. Transit time does not excuse this.',
evidence: [msgEvidence(m)],
})
reject.push(m)
rejected = true
}
if (verdict === 'unknown') {
findings.push({
id: `${m.id}/key`,
summary: `${key} carries no recognised signing key`,
determination: 'not-assessed',
severity: 'high',
detail: 'Nothing establishes who sent this. An unsigned message on an intermittent link is not distinguishable from an injected one.',
evidence: [msgEvidence(m)],
})
reject.push(m)
rejected = true
}
if (verdict === 'expired-in-flight') {
// Reported, NOT rejected. On a link with hours of transit this is the
// normal case, and dropping it would discard legitimate traffic.
findings.push({
id: `${m.id}/key-flight`,
summary: `${key} was signed with a key that expired while in flight`,
determination: 'not-applicable',
severity: 'info',
detail: 'Valid when sent, expired on arrival. Accepted, because rejecting it would drop legitimate traffic on any link with real transit time — but recorded, because the key is no longer trusted for anything new.',
evidence: [msgEvidence(m)],
})
}
if (seen.has(key)) {
const idempotent = m.idempotent ?? policy.receiverIsIdempotent ?? false
findings.push({
id: `${m.id}/duplicate`,
summary: `${key} was delivered more than once`,
determination: idempotent ? 'not-applicable' : 'fail',
severity: idempotent ? 'info' : 'critical',
detail: idempotent
? 'A duplicate is the at-least-once delivery guarantee working, and the receiver is idempotent. Not a fault.'
: 'At-least-once delivery meeting a handler that has not declared itself idempotent. The duplicate is not the anomaly — the assumption of exactly-once is.',
evidence: [msgEvidence(m)],
})
if (!idempotent) reject.push(m)
continue
}
seen.add(key)
const hi = highest.get(m.sender)
if (hi !== undefined && m.sequence < hi) {
findings.push({
id: `${m.id}/order`,
summary: `${key} arrived after ${m.sender}#${hi} and is older`,
determination: 'fail',
severity: 'high',
detail: 'Applying this in arrival order would overwrite newer state with older, and would look like it worked. Arrival order is a property of the link, not of the sender.',
evidence: [msgEvidence(m)],
})
reject.push(m)
rejected = true
} else {
highest.set(m.sender, m.sequence)
}
if (t.seconds !== null && policy.maxTransitSeconds && t.seconds > policy.maxTransitSeconds) {
findings.push({
id: `${m.id}/stale`,
summary: `${key} spent ${Math.round(t.seconds / 60)} minutes in transit`,
determination: 'fail',
severity: 'high',
detail: `Beyond the ${Math.round(policy.maxTransitSeconds / 60)}-minute limit for acting on a message. Record it; do not act on it — the world it described has moved on.`,
evidence: [msgEvidence(m)],
})
reject.push(m)
rejected = true
}
if (!rejected) {
apply.push(m)
findings.push({
id: m.id,
summary: `${key} accepted${t.seconds !== null ? `, ${t.seconds}s in transit` : ''}`,
determination: 'pass',
severity: 'info',
evidence: [msgEvidence(m)],
})
}
}
return {
report: {
capability: 'SlowLane',
scope: `${list.length} message(s) from ${new Set(list.map(m => m.sender)).size} sender(s)`,
examined: list.length,
findings,
notes: [
`${apply.length} to apply, ${reject.length} rejected — apply order is by sender sequence, NOT arrival`,
],
},
reject,
apply,
}
}
/** Render a stream assessment for a terminal, log or review. */
export function formatStream(messages: Message[], policy: LinkPolicy = {}, opts: FormatOptions = {}): string {
return renderReport(assessStream(messages, policy).report, opts)
}
+154
View File
@@ -0,0 +1,154 @@
import { describe, expect, it } from 'vitest'
import { overall, validateReport } from '@extant2000/evidence-record'
import {
assessStream,
formatStream,
keyVerdict,
transit,
type LinkPolicy,
type Message,
} from '../src/index.js'
const msg = (p: Partial<Message> = {}): Message => ({
id: 'm1',
sender: 'probe-a',
sequence: 1,
originAt: '2026-08-01T10:00:00Z',
receivedAt: '2026-08-01T10:20:00Z',
keyId: 'k1',
where: 'downlink.log',
...p,
})
const policy: LinkPolicy = { keys: [{ id: 'k1' }] }
describe('expiry has two ends and one check cannot express both', () => {
const expiring: LinkPolicy = { keys: [{ id: 'k1', notAfter: '2026-08-01T10:10:00Z' }] }
it('calls a key that expired IN FLIGHT its own thing', () => {
// Sent 10:00 (valid), arrived 10:20 (expired at 10:10).
expect(keyVerdict(msg(), expiring)).toBe('expired-in-flight')
})
it('ACCEPTS it, because rejecting would drop legitimate traffic', () => {
const { apply, report } = assessStream([msg()], expiring)
expect(apply).toHaveLength(1)
const f = report.findings.find(x => x.id.endsWith('/key-flight'))!
expect(f.determination).toBe('not-applicable')
expect(f.detail).toContain('rejecting it would drop legitimate traffic')
})
it('still records it, because the key is no longer trusted for anything new', () => {
expect(formatStream([msg()], expiring)).toContain('expired while in flight')
})
it('rejects a key that was already invalid at origin', () => {
const late = msg({ originAt: '2026-08-01T10:15:00Z', receivedAt: '2026-08-01T10:30:00Z' })
expect(keyVerdict(late, expiring)).toBe('invalid-at-origin')
expect(assessStream([late], expiring).reject).toHaveLength(1)
})
it('revocation ignores origin time entirely', () => {
// The key was usually compromised before anyone noticed.
const revoked: LinkPolicy = { keys: [{ id: 'k1', revokedAt: '2026-08-01T23:00:00Z' }] }
expect(keyVerdict(msg(), revoked)).toBe('revoked')
const r = assessStream([msg()], revoked)
expect(r.reject).toHaveLength(1)
expect(r.report.findings[0]!.detail).toContain('Revocation is not a schedule')
})
it('rejects an unrecognised or absent key as unassessed', () => {
expect(keyVerdict(msg({ keyId: undefined }), policy)).toBe('unknown')
expect(keyVerdict(msg({ keyId: 'nope' }), policy)).toBe('unknown')
const r = assessStream([msg({ keyId: undefined })], policy)
expect(r.report.findings[0]!.determination).toBe('not-assessed')
expect(r.reject).toHaveLength(1)
})
})
describe('ordering is by sender sequence, never by arrival', () => {
it('rejects an older message that arrived after a newer one', () => {
// Applying in arrival order would overwrite newer state with older,
// and would look like it worked.
const out = [
msg({ id: 'm2', sequence: 2, originAt: '2026-08-01T10:05:00Z', receivedAt: '2026-08-01T10:10:00Z' }),
msg({ id: 'm1', sequence: 1 }),
]
const r = assessStream(out, policy)
// Sequence 1 sorts first, so 2 is applied and there is no regression.
expect(r.apply.map(m => m.sequence)).toEqual([1, 2])
})
it('returns apply order by sequence even when the input is shuffled', () => {
const shuffled = [
msg({ id: 'c', sequence: 3 }),
msg({ id: 'a', sequence: 1 }),
msg({ id: 'b', sequence: 2 }),
]
expect(assessStream(shuffled, policy).apply.map(m => m.sequence)).toEqual([1, 2, 3])
})
it('tracks sequences per sender, not globally', () => {
const two = [
msg({ id: 'a', sender: 'probe-a', sequence: 9 }),
msg({ id: 'b', sender: 'probe-b', sequence: 1 }),
]
expect(assessStream(two, policy).reject).toHaveLength(0)
})
})
describe('a duplicate is the delivery guarantee working', () => {
const dup = [msg({ id: 'a', sequence: 1 }), msg({ id: 'b', sequence: 1 })]
it('fails a duplicate when the receiver is not idempotent', () => {
const r = assessStream(dup, policy)
const f = r.report.findings.find(x => x.id.endsWith('/duplicate'))!
expect(f.determination).toBe('fail')
expect(f.detail).toContain('the assumption of exactly-once is')
})
it('accepts it when the receiver declares idempotency', () => {
const r = assessStream(dup, { ...policy, receiverIsIdempotent: true })
const f = r.report.findings.find(x => x.id.endsWith('/duplicate'))!
expect(f.determination).toBe('not-applicable')
expect(r.reject).toHaveLength(0)
})
it('accepts it when the message itself is idempotent', () => {
const r = assessStream([msg({ id: 'a', idempotent: true }), msg({ id: 'b', idempotent: true })], policy)
expect(r.reject).toHaveLength(0)
})
})
describe('transit', () => {
it('rejects a message too old to act on, and says to record it anyway', () => {
const r = assessStream([msg()], { ...policy, maxTransitSeconds: 300 })
const f = r.report.findings.find(x => x.id.endsWith('/stale'))!
expect(f.determination).toBe('fail')
expect(f.detail).toContain('Record it; do not act on it')
})
it('flags impossible clocks as unassessed, not as a fast message', () => {
const back = msg({ originAt: '2026-08-01T10:20:00Z', receivedAt: '2026-08-01T10:00:00Z' })
expect(transit(back).impossible).toBe(true)
const r = assessStream([back], policy)
expect(r.report.findings[0]!.determination).toBe('not-assessed')
expect(r.report.findings[0]!.detail).toContain('neither transit time nor any expiry decision')
})
})
describe('conformance with the evidence-record standard', () => {
it('an empty stream is not-assessed, and says what silence means', () => {
const r = assessStream([])
expect(overall(r.report)).toBe('not-assessed')
expect(formatStream([])).toContain('silence is not evidence that nothing was sent')
})
it('every conclusion carries a citation', () => {
expect(validateReport(assessStream([msg(), msg({ id: 'b', sequence: 2 })], policy).report)).toEqual([])
})
it('passes a clean stream', () => {
expect(overall(assessStream([msg()], policy).report)).toBe('pass')
})
})
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist-cjs",
"declaration": false
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
"declaration": true, "outDir": "dist", "rootDir": "src",
"strict": true, "skipLibCheck": true
},
"include": ["src/**/*.ts"]
}