mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
ci(release): stop on a stale dialect, check it like CI would, give prereleases their own
A stable release without a dialect of its own now ships the one on main only while that one has 90 days left; dialect 0 or less stops the run before anything is committed, unless allow_stale_dialect is ticked. Fetching and checking moved into registration-dialect.yml, which never fails its caller and also type-checks the module under the strict config and lints it with --max-warnings 0, since the bump commit skips CI. The bump and everything after it run whatever became of the dialect job. Prereleases ask the issuer for a dialect of their -pre.N version and build it into the image without committing it. The release check reads the module token by token against an allowlist of keywords, globals and member names, refuses any assignment to a property, computed access and regular expressions, and names Object, JSON and defineProperty among the forbidden. The issuer's kitchen-sink sample is gone; the tests use a hand-written dialect instead.
This commit is contained in:
@@ -6,9 +6,12 @@ name: Build & Push Docker Image (Prerelease)
|
||||
# release it refuses a commit whose ci-ok is not green. docker.yml with
|
||||
# bump=auto later finalizes the line once main carries it.
|
||||
#
|
||||
# A prerelease gets no registration dialect of its own; it carries the one of
|
||||
# the last stable release, which dev gets by merging main into dev after that
|
||||
# release. The build warns when that dialect is the original one or old.
|
||||
# A prerelease asks the TREK services issuer for a registration dialect of its
|
||||
# own -pre.N version (registration-dialect.yml, as the stable release does)
|
||||
# and builds it into the image; nothing of it is committed, dev keeps the
|
||||
# module of the last stable release, which it gets when main is merged back
|
||||
# into dev. Without one the image carries dev's module, with a warning when
|
||||
# that is the original one or old.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -93,31 +96,23 @@ jobs:
|
||||
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "$STABLE to $NEW_VERSION"
|
||||
|
||||
# A prerelease registers new installations in the dialect of the last
|
||||
# stable release, which reaches dev when main is merged into dev after
|
||||
# the release. The issuer takes a dialect for 180 days, so this warns
|
||||
# (without stopping the build) while dev still speaks the original
|
||||
# registration or a dialect older than 150 days.
|
||||
- name: Check the registration dialect's age
|
||||
run: |
|
||||
set -eu
|
||||
file=server/src/nest/service-identity/registration-dialect.ts
|
||||
id="$(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$file")"
|
||||
created="$(sed -n "s/^ createdAt: '\(.*\)',$/\1/p" "$file")"
|
||||
if [ "$id" = 0 ]; then
|
||||
message="This build registers in the original dialect, which the issuer accepts only for a transition period. Merge main into dev once a stable release carries a dialect of its own."
|
||||
else
|
||||
age=$(( ($(date -u +%s) - $(date -u -d "$created" +%s)) / 86400 ))
|
||||
[ "$age" -gt 150 ] || { echo "This build registers in dialect $id from $created ($age days old)."; exit 0; }
|
||||
message="This build registers in dialect $id from $created, $age days old; new installations of it stop registering on $(date -u -d "$created + 180 days" +%Y-%m-%d). Merge main into dev after the next stable release."
|
||||
fi
|
||||
echo "::warning::$message"
|
||||
printf '### Registration dialect\n%s\n' "$message" >> "$GITHUB_STEP_SUMMARY"
|
||||
# The registration dialect of this prerelease, fetched and checked in a job
|
||||
# without write access. It never fails the build.
|
||||
dialect:
|
||||
needs: version-bump
|
||||
uses: ./.github/workflows/registration-dialect.yml
|
||||
with:
|
||||
version: ${{ needs.version-bump.outputs.version }}
|
||||
ref: ${{ needs.version-bump.outputs.sha }}
|
||||
secrets:
|
||||
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
|
||||
|
||||
build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 120
|
||||
needs: version-bump
|
||||
needs: [version-bump, dialect]
|
||||
# Whatever became of the dialect job: without its artifact the image carries dev's module.
|
||||
if: ${{ !cancelled() && needs.version-bump.result == 'success' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -136,6 +131,48 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.version-bump.outputs.sha }}
|
||||
|
||||
- name: Take over the prerelease's dialect
|
||||
id: take
|
||||
uses: actions/download-artifact@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: registration-dialect
|
||||
path: ${{ runner.temp }}/dialect
|
||||
|
||||
# Into the build context only; the commit stays as it is. Without a
|
||||
# dialect of its own the image speaks dev's module, which the issuer
|
||||
# takes for new installations for 180 days after it was made (longer
|
||||
# while it is the newest stable release's). That is warned about, never
|
||||
# stopped on, and a date that cannot be read only warns as well.
|
||||
- name: Place the dialect
|
||||
env:
|
||||
VERSION: ${{ needs.version-bump.outputs.version }}
|
||||
TAKEN: ${{ steps.take.outcome }}
|
||||
REPORT: ${{ matrix.platform == 'linux/amd64' }}
|
||||
run: |
|
||||
set -u
|
||||
target=server/src/nest/service-identity/registration-dialect.ts
|
||||
fetched="$RUNNER_TEMP/dialect/registration-dialect.ts"
|
||||
if [ "$TAKEN" = success ] && [ -f "$fetched" ]; then
|
||||
cp "$fetched" "$target"
|
||||
echo "$VERSION registers in dialect $(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$target") of its own."
|
||||
exit 0
|
||||
fi
|
||||
[ "$REPORT" = true ] || exit 0
|
||||
id="$(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$target")"
|
||||
created="$(sed -n "s/^ createdAt: '\(.*\)',$/\1/p" "$target")"
|
||||
if [ "$id" = 0 ]; then
|
||||
message="$VERSION got no dialect of its own and registers in the original one, which the issuer accepts only for a transition period."
|
||||
elif made="$(date -u -d "$created" +%s 2>/dev/null)"; then
|
||||
age=$(( ($(date -u +%s) - made) / 86400 ))
|
||||
message="$VERSION got no dialect of its own and registers in dev's dialect $id from $created, $age days old."
|
||||
[ "$age" -gt 150 ] || { echo "$message"; exit 0; }
|
||||
else
|
||||
message="$VERSION got no dialect of its own, and the date of dev's dialect cannot be read from $target."
|
||||
fi
|
||||
echo "::warning::$message"
|
||||
printf '### Registration dialect\n%s\n' "$message" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
@@ -181,6 +218,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
needs: [version-bump, build]
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
|
||||
@@ -4,8 +4,9 @@ name: Build & Push Docker Image
|
||||
# 1. refuses to run unless ci-ok passed on the main commit it releases,
|
||||
# 2. works out the version from the tags and the bump input,
|
||||
# 3. asks the TREK services issuer for the registration dialect of that
|
||||
# version and checks it, in a job without write access (a release without
|
||||
# one ships the dialect already on main, with a warning),
|
||||
# version and checks it, in a job without write access
|
||||
# (registration-dialect.yml); without one the release ships the dialect
|
||||
# already on main, but only while that one still has 90 days to run,
|
||||
# 4. commits the bump to package.json, the lockfile, Chart.yaml and the
|
||||
# dialect, tags it and pushes,
|
||||
# 5. builds both platforms from exactly that bump commit (never from
|
||||
@@ -19,10 +20,11 @@ name: Build & Push Docker Image
|
||||
# someone picks patch, minor or major.
|
||||
#
|
||||
# Each stable release registers new installations in a dialect of its own,
|
||||
# which the issuer accepts for 180 days; renewing a key needs none. dev and
|
||||
# the prereleases built from it carry the dialect of the last stable release,
|
||||
# so after a release, merge main into dev. docker-dev.yml warns when dev's
|
||||
# dialect is the original one or getting old.
|
||||
# which the issuer accepts for 180 days, and for as long as it is the newest
|
||||
# release's; renewing a key needs none. dev gets the module when main is merged
|
||||
# back into dev after the release; a prerelease asks the issuer for one of its
|
||||
# own (docker-dev.yml). The issuer's side, and what to set up before the first
|
||||
# release with a dialect, is in the operations guide of the TREK services.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -36,6 +38,10 @@ on:
|
||||
description: "Type MAJOR (all caps) to confirm a major release"
|
||||
type: string
|
||||
default: ''
|
||||
allow_stale_dialect:
|
||||
description: 'Ship without a dialect of its own even if the one on main is the original one or has under 90 days left'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -155,90 +161,23 @@ jobs:
|
||||
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "$STABLE to $NEW_VERSION ($BUMP)"
|
||||
|
||||
# Fetches the registration dialect of this version from the TREK services
|
||||
# issuer and checks it (scripts/ci/check-registration-dialect.mjs). The
|
||||
# issuer's answer runs here, in a job that holds no write token and keeps no
|
||||
# git credentials; only the checked file goes on, as an artifact. The job
|
||||
# never fails the release: without a dialect, the release ships the one on
|
||||
# main, which the issuer accepts for 180 days after it was made.
|
||||
# The registration dialect of this version, fetched and checked in a job
|
||||
# without write access. It never fails; without an artifact from it the
|
||||
# bump decides below whether the dialect on main may ship.
|
||||
dialect:
|
||||
needs: version
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
# Every step either cannot fail or is allowed to, so this job always
|
||||
# succeeds and never holds up the bump.
|
||||
- uses: actions/checkout@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
ref: ${{ needs.version.outputs.base }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
# The only step that sees the token. The URL is fixed; the answer is
|
||||
# bounded in time and size and goes to a file nothing runs yet.
|
||||
- name: Fetch the dialect from the issuer
|
||||
id: fetch
|
||||
env:
|
||||
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
out="$RUNNER_TEMP/registration-dialect.ts"
|
||||
if [ -z "${TREK_DIALECT_TOKEN:-}" ]; then
|
||||
echo "::warning::TREK_DIALECT_TOKEN is not set; $VERSION ships the registration dialect already on main."
|
||||
exit 0
|
||||
fi
|
||||
status=$(curl --silent --show-error --proto '=https' --tlsv1.2 --max-time 30 --retry 2 --retry-delay 5 \
|
||||
--max-filesize 262144 -H "Authorization: Bearer $TREK_DIALECT_TOKEN" -H 'Content-Type: application/json' \
|
||||
--data "{\"version\":\"$VERSION\"}" -o "$out" -w '%{http_code}' https://places.liketrek.com/v1/dialects) \
|
||||
|| status=000
|
||||
if [ "$status" != 200 ] && [ "$status" != 201 ]; then
|
||||
echo "::warning::The issuer handed out no registration dialect for $VERSION (HTTP $status); it ships the one on main."
|
||||
rm -f "$out"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$(wc -c < "$out")" -gt 262144 ]; then
|
||||
echo "::warning::The issuer's registration dialect for $VERSION is larger than a dialect; it ships the one on main."
|
||||
rm -f "$out"
|
||||
exit 0
|
||||
fi
|
||||
echo "file=$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check the dialect
|
||||
id: check
|
||||
if: steps.fetch.outputs.file != ''
|
||||
env:
|
||||
FILE: ${{ steps.fetch.outputs.file }}
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
if ! node scripts/ci/check-registration-dialect.mjs "$FILE" "$VERSION"; then
|
||||
echo "::warning::The issuer's registration dialect for $VERSION failed its check; $VERSION ships the one on main."
|
||||
exit 0
|
||||
fi
|
||||
echo "What changes against the dialect on main:"
|
||||
git diff --no-index -- server/src/nest/service-identity/registration-dialect.ts "$FILE" || true
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Hand the dialect to the bump
|
||||
if: steps.check.outputs.ok == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: registration-dialect
|
||||
path: ${{ steps.fetch.outputs.file }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
uses: ./.github/workflows/registration-dialect.yml
|
||||
with:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
ref: ${{ needs.version.outputs.base }}
|
||||
secrets:
|
||||
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
|
||||
|
||||
# Runs whatever became of the dialect job, a timeout or a lost runner
|
||||
# included: no artifact means the dialect on main, if it may still ship.
|
||||
version-bump:
|
||||
needs: [version, dialect]
|
||||
if: ${{ !cancelled() && needs.version.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
outputs:
|
||||
@@ -265,31 +204,56 @@ jobs:
|
||||
name: registration-dialect
|
||||
path: ${{ runner.temp }}/dialect
|
||||
|
||||
# Without a dialect of its own the release ships the one on main, but
|
||||
# only while the issuer takes new registrations in it for 90 more days:
|
||||
# dialect 0 closes on the issuer's transition date, and a dialect with
|
||||
# little left would shut out new installations of this release soon after
|
||||
# it is out. Then this stops before anything is committed, unless
|
||||
# allow_stale_dialect says to ship anyway. The issuer keeps the newest
|
||||
# release's dialect open past its date, so the date here is the least it
|
||||
# has, not the most.
|
||||
- name: Place the dialect
|
||||
env:
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
TAKEN: ${{ steps.take.outcome }}
|
||||
ALLOW_STALE: ${{ inputs.allow_stale_dialect }}
|
||||
run: |
|
||||
set -eu
|
||||
set -u
|
||||
target=server/src/nest/service-identity/registration-dialect.ts
|
||||
fetched="$RUNNER_TEMP/dialect/registration-dialect.ts"
|
||||
dialect_id() { sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$1"; }
|
||||
report() {
|
||||
echo "$2$1"
|
||||
printf '### Registration dialect\n%s\n' "$1" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
if [ "$TAKEN" = success ] && [ -f "$fetched" ]; then
|
||||
cp "$fetched" "$target"
|
||||
message="$VERSION registers in dialect $(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$target"), written by the issuer for it."
|
||||
echo "$message"
|
||||
printf '### Registration dialect\n%s\n' "$message" >> "$GITHUB_STEP_SUMMARY"
|
||||
report "$VERSION registers in dialect $(dialect_id "$target"), written by the issuer for it." ""
|
||||
exit 0
|
||||
fi
|
||||
id="$(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$target")"
|
||||
id="$(dialect_id "$target")"
|
||||
created="$(sed -n "s/^ createdAt: '\(.*\)',$/\1/p" "$target")"
|
||||
if [ "$id" = 0 ]; then
|
||||
until="the end of the transition period the issuer allows the original registration"
|
||||
else
|
||||
until="$(date -u -d "$created + 180 days" +%Y-%m-%d)"
|
||||
left=""
|
||||
if [ -n "$id" ] && [ "$id" != 0 ] && made="$(date -u -d "$created" +%s 2>/dev/null)"; then
|
||||
left=$(( (made + 180 * 86400 - $(date -u +%s)) / 86400 ))
|
||||
fi
|
||||
message="$VERSION ships without a dialect of its own and registers in dialect $id from $created, which the issuer accepts for new installations until $until. The issuer's operator can write the dialect by hand; it then ships with the next patch."
|
||||
echo "::warning::$message"
|
||||
printf '### Registration dialect\n%s\n' "$message" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -n "$left" ] && [ "$left" -ge 90 ]; then
|
||||
report "$VERSION ships without a dialect of its own and registers in dialect $id from $created, which the issuer takes for new installations for $left more days at least. Its operator can write one by hand for the next patch." "::warning::"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$id" = 0 ]; then
|
||||
what="the original registration, which the issuer accepts only until the end of its transition period"
|
||||
elif [ -n "$left" ]; then
|
||||
what="dialect $id from $created, which has $left days left"
|
||||
else
|
||||
what="a dialect whose id or date cannot be read from $target"
|
||||
fi
|
||||
if [ "$ALLOW_STALE" = true ]; then
|
||||
report "$VERSION ships without a dialect of its own and registers in $what, as allow_stale_dialect asked." "::warning::"
|
||||
exit 0
|
||||
fi
|
||||
report "$VERSION got no dialect of its own and would register in $what. Nothing was committed or pushed. Make sure the issuer runs with its dialect seed and release token and that TREK_DIALECT_TOKEN is set, then run the release again, or tick allow_stale_dialect to ship anyway." "::error::"
|
||||
exit 1
|
||||
|
||||
- name: Update the version, commit, tag and push
|
||||
id: commit
|
||||
@@ -321,6 +285,8 @@ jobs:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 120
|
||||
needs: [version, version-bump]
|
||||
# version-bump may run after a failed dialect job; that alone must not skip what follows it.
|
||||
if: ${{ !cancelled() && needs.version-bump.result == 'success' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -384,6 +350,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
needs: [version, version-bump, build]
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -448,6 +415,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
needs: version-bump
|
||||
if: ${{ !cancelled() && needs.version-bump.result == 'success' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
name: Registration dialect
|
||||
|
||||
# Asks the TREK services issuer for the registration dialect of one version and
|
||||
# checks it, for docker.yml (a stable release commits it with the version bump)
|
||||
# and docker-dev.yml (a prerelease only builds it into its image).
|
||||
#
|
||||
# The issuer's answer runs here, in a job that holds no write token and keeps
|
||||
# no git credentials; the token for the issuer is in the environment of the
|
||||
# one step that fetches. The answer is held to what a dialect is
|
||||
# (scripts/ci/check-registration-dialect.mjs), then type-checked and linted
|
||||
# the way TREK's CI would see it once it is on main and dev. Only a module that
|
||||
# passes all of it goes on, as the artifact `registration-dialect`; without
|
||||
# one the caller decides what ships. This workflow never fails its caller.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The version the dialect is for, X.Y.Z or X.Y.Z-pre.N'
|
||||
type: string
|
||||
required: true
|
||||
ref:
|
||||
description: 'The commit whose checker, types and lint config the dialect is checked with'
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
TREK_DIALECT_TOKEN:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dialect:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
# A failed or timed-out run leaves no artifact, which the caller already
|
||||
# handles; it must not fail the release around it.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
# The only step that sees the token. The URL is fixed; the answer is
|
||||
# bounded in time and size and goes to a file nothing runs yet.
|
||||
- name: Fetch the dialect from the issuer
|
||||
id: fetch
|
||||
timeout-minutes: 3
|
||||
env:
|
||||
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
out="$RUNNER_TEMP/registration-dialect.ts"
|
||||
if [ -z "${TREK_DIALECT_TOKEN:-}" ]; then
|
||||
echo "::warning::TREK_DIALECT_TOKEN is not set; the issuer was not asked for a dialect for $VERSION."
|
||||
exit 0
|
||||
fi
|
||||
status=$(curl --silent --show-error --proto '=https' --tlsv1.2 --max-time 30 --retry 2 --retry-delay 5 \
|
||||
--max-filesize 262144 -H "Authorization: Bearer $TREK_DIALECT_TOKEN" -H 'Content-Type: application/json' \
|
||||
--data "{\"version\":\"$VERSION\"}" -o "$out" -w '%{http_code}' https://places.liketrek.com/v1/dialects) \
|
||||
|| status=000
|
||||
if [ "$status" != 200 ] && [ "$status" != 201 ]; then
|
||||
echo "::warning::The issuer handed out no registration dialect for $VERSION (HTTP $status)."
|
||||
rm -f "$out"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$(wc -c < "$out")" -gt 262144 ]; then
|
||||
echo "::warning::The issuer's registration dialect for $VERSION is larger than a dialect."
|
||||
rm -f "$out"
|
||||
exit 0
|
||||
fi
|
||||
echo "file=$out" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# A module that computes for long is refused by the clock as well.
|
||||
- name: Check the dialect
|
||||
id: check
|
||||
if: steps.fetch.outputs.file != ''
|
||||
timeout-minutes: 2
|
||||
env:
|
||||
FILE: ${{ steps.fetch.outputs.file }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
if ! node scripts/ci/check-registration-dialect.mjs "$FILE" "$VERSION"; then
|
||||
echo "::warning::The issuer's registration dialect for $VERSION failed its check."
|
||||
exit 0
|
||||
fi
|
||||
echo "What changes against the dialect in the tree:"
|
||||
git diff --no-index -- server/src/nest/service-identity/registration-dialect.ts "$FILE" || true
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install the toolchain
|
||||
if: steps.check.outputs.ok == 'true'
|
||||
timeout-minutes: 8
|
||||
run: |
|
||||
npm ci --ignore-scripts --no-audit --no-fund
|
||||
npm run build --workspace=shared
|
||||
|
||||
# The bump commit skips CI, so this is where the module meets the strict
|
||||
# config (lint:strict lets a new file have no strict errors) and ESLint
|
||||
# with --max-warnings 0 (lint:warnings starts a new file at none) before
|
||||
# it lands on main.
|
||||
- name: Type-check and lint the dialect
|
||||
id: types
|
||||
if: steps.check.outputs.ok == 'true'
|
||||
timeout-minutes: 5
|
||||
working-directory: server
|
||||
env:
|
||||
FILE: ${{ steps.fetch.outputs.file }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
cp "$FILE" src/nest/service-identity/registration-dialect.ts
|
||||
printf '%s\n' '{"extends": "./tsconfig.strict.json", "include": [],' \
|
||||
'"files": ["src/nest/service-identity/registration-dialect.ts"]}' > tsconfig.dialect-check.json
|
||||
if ! npx tsc -p tsconfig.dialect-check.json --noEmit --pretty false; then
|
||||
echo "::warning::The issuer's registration dialect for $VERSION does not type-check under the strict config."
|
||||
exit 0
|
||||
fi
|
||||
if ! npx eslint --max-warnings 0 src/nest/service-identity/registration-dialect.ts; then
|
||||
echo "::warning::The issuer's registration dialect for $VERSION does not pass ESLint."
|
||||
exit 0
|
||||
fi
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Hand the dialect on
|
||||
if: steps.types.outputs.ok == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: registration-dialect
|
||||
path: ${{ steps.fetch.outputs.file }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -1,23 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// Checks a registration dialect the issuer handed out, before a release ships it.
|
||||
//
|
||||
// docker.yml asks the TREK services issuer for the dialect of the version it
|
||||
// releases and commits the answer as
|
||||
// server/src/nest/service-identity/registration-dialect.ts. That file then
|
||||
// runs inside every TREK server, so it is held to what a dialect is: a few
|
||||
// hashes over the inputs it is given, nothing else.
|
||||
// docker.yml (through registration-dialect.yml) asks the TREK services issuer
|
||||
// for the dialect of the version it releases and commits the answer as
|
||||
// server/src/nest/service-identity/registration-dialect.ts, without a person
|
||||
// reading it first. That file then runs inside every TREK server, so it is
|
||||
// held to what a dialect is: a few hashes over the inputs it is given, and
|
||||
// nothing that reaches past them.
|
||||
//
|
||||
// 1. Statically, before anything of it runs: the size, the header comment,
|
||||
// imports from node:crypto, node:zlib and the types module only, none of
|
||||
// the names that reach the process, the network, the file system, timers
|
||||
// or code from strings, no computed property access and no backslash (so
|
||||
// no escape can spell a forbidden name), and the version and id it must
|
||||
// carry. The generated code needs none of these.
|
||||
// 1. Statically, before anything of it runs. The size, the header comment,
|
||||
// no backslash (so no escape can spell a name), imports from node:crypto,
|
||||
// node:zlib and the types module only, and the version and id it must
|
||||
// carry. Then the code is read token by token against what it may hold,
|
||||
// not against what it may not: the few keywords the generator writes,
|
||||
// names it declares itself or imports, a handful of globals (Buffer,
|
||||
// Math, String, BigInt, Array, parseInt), and after a dot only the Buffer,
|
||||
// array, string, hash and Math members the generator uses. It may assign
|
||||
// to its own names only, never to a property (no Math.random = ...,
|
||||
// Buffer.from = ..., no defineProperty at all), read no property by a
|
||||
// computed name and hold no regular expression. A list of names that reach
|
||||
// the process, the network, timers or code from strings is checked on top.
|
||||
// 2. Then it is imported (Node strips the types) and run on the fixed inputs
|
||||
// in its SELF_CHECK: the password, the salt, scrypt over them and the
|
||||
// request must be what the issuer computed, and the request must pass
|
||||
// the rules the issuer client applies (issuer.client.ts holds the same).
|
||||
//
|
||||
// A new construct in the issuer's generator needs its name added here first,
|
||||
// in a release before the first dialect that uses it.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/ci/check-registration-dialect.mjs <file.ts> <version>
|
||||
//
|
||||
@@ -35,8 +45,9 @@ const TYPES_MODULE = './registration-dialect.types';
|
||||
const RUNTIME_MODULES = new Set(['node:crypto', 'node:zlib']);
|
||||
const IMPORT_LINE = /^import (type )?\{[\w\s,]+\} from '([^']+)';$/;
|
||||
|
||||
// Each of these reaches past the hashes: the process and its environment,
|
||||
// modules, the network, timers, or a way to run a string as code.
|
||||
// Checked on every line, on top of the allowlist below: each of these reaches
|
||||
// past the hashes, into the process and its environment, modules, the
|
||||
// network, timers, the globals' definitions, or a way to run a string as code.
|
||||
const FORBIDDEN = [
|
||||
/\bprocess\b/,
|
||||
/\brequire\b/,
|
||||
@@ -63,10 +74,126 @@ const FORBIDDEN = [
|
||||
/\b__proto__\b/,
|
||||
/\bReflect\b/,
|
||||
/\bProxy\b/,
|
||||
/\bObject\b/,
|
||||
/\bJSON\b/,
|
||||
/\bSymbol\b/,
|
||||
/\bdefineProperty\b/,
|
||||
/\bsetPrototypeOf\b/,
|
||||
/\bexport\s+default\b/,
|
||||
];
|
||||
// x[...] and x?.[...]; a type annotation's T[] is all the generated code holds.
|
||||
const COMPUTED_ACCESS = /([\w$)\]]\s*|\?\.\s*)\[(?!\])/;
|
||||
|
||||
// The keywords a dialect is written with.
|
||||
const KEYWORDS = new Set([
|
||||
'const',
|
||||
'let',
|
||||
'function',
|
||||
'return',
|
||||
'for',
|
||||
'do',
|
||||
'while',
|
||||
'export',
|
||||
'import',
|
||||
'type',
|
||||
'from',
|
||||
]);
|
||||
// Globals and type names it may name without declaring them.
|
||||
const GLOBALS = new Set(['Buffer', 'Math', 'String', 'BigInt', 'Array', 'parseInt', 'number', 'string']);
|
||||
// What it may read after a dot: the challenge's c, and the members of Buffer,
|
||||
// arrays, strings, hashes and Math the generator writes.
|
||||
const PROPERTIES = new Set([
|
||||
'c',
|
||||
'length',
|
||||
'from',
|
||||
'alloc',
|
||||
'concat',
|
||||
'subarray',
|
||||
'slice',
|
||||
'copy',
|
||||
'reverse',
|
||||
'map',
|
||||
'forEach',
|
||||
'push',
|
||||
'join',
|
||||
'padStart',
|
||||
'padEnd',
|
||||
'charAt',
|
||||
'toString',
|
||||
'toUpperCase',
|
||||
'update',
|
||||
'digest',
|
||||
'readUInt8',
|
||||
'writeUInt8',
|
||||
'readUInt32BE',
|
||||
'readUInt32LE',
|
||||
'writeUInt16BE',
|
||||
'writeUInt16LE',
|
||||
'writeUInt32BE',
|
||||
'writeUInt32LE',
|
||||
'writeBigUInt64BE',
|
||||
'writeBigUInt64LE',
|
||||
'floor',
|
||||
'sign',
|
||||
'max',
|
||||
'min',
|
||||
]);
|
||||
// Every punctuator the generator writes, longest first. `?`, `?.`, `??`, `#`
|
||||
// and `@` are not among them.
|
||||
const PUNCTUATORS = [
|
||||
'>>>=',
|
||||
'...',
|
||||
'===',
|
||||
'!==',
|
||||
'**=',
|
||||
'<<=',
|
||||
'>>=',
|
||||
'>>>',
|
||||
'=>',
|
||||
'==',
|
||||
'!=',
|
||||
'<=',
|
||||
'>=',
|
||||
'&&',
|
||||
'||',
|
||||
'++',
|
||||
'--',
|
||||
'+=',
|
||||
'-=',
|
||||
'*=',
|
||||
'/=',
|
||||
'%=',
|
||||
'&=',
|
||||
'|=',
|
||||
'^=',
|
||||
'**',
|
||||
'<<',
|
||||
'>>',
|
||||
'{',
|
||||
'}',
|
||||
'(',
|
||||
')',
|
||||
'[',
|
||||
']',
|
||||
';',
|
||||
',',
|
||||
'.',
|
||||
'<',
|
||||
'>',
|
||||
'+',
|
||||
'-',
|
||||
'*',
|
||||
'/',
|
||||
'%',
|
||||
'&',
|
||||
'|',
|
||||
'^',
|
||||
'!',
|
||||
'~',
|
||||
'=',
|
||||
':',
|
||||
];
|
||||
const ASSIGNMENTS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '|=', '^=']);
|
||||
// After one of these (or at the start) a `/` would open a regular expression.
|
||||
const BEFORE_EXPRESSION = new Set([...PUNCTUATORS.filter((p) => p !== ')' && p !== ']'), 'return']);
|
||||
|
||||
const CHALLENGE_PATH = /^\/v1\/register(\/[A-Za-z0-9_-]{12})?\/challenge$/;
|
||||
const REGISTER_PATH = /^\/v1\/register(\/[A-Za-z0-9_-]{12})?$/;
|
||||
@@ -78,6 +205,170 @@ const LARGEST_NONCE = 2 ** 40;
|
||||
|
||||
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// The source as tokens: { kind: 'name' | 'number' | 'string' | 'punct', value, line }.
|
||||
// Comments go, a template literal becomes a string token around the tokens of
|
||||
// each ${...} in it. Throws on anything else, a regular expression included.
|
||||
export function tokenize(source) {
|
||||
const tokens = [];
|
||||
const braces = [];
|
||||
let i = 0;
|
||||
let line = 1;
|
||||
const push = (kind, value) => tokens.push({ kind, value, line });
|
||||
const fail = (what) => {
|
||||
throw new Error(`line ${line} holds ${what}`);
|
||||
};
|
||||
const template = () => {
|
||||
// From just after a ` or a }, up to the closing ` or the next ${.
|
||||
while (i < source.length && source[i] !== '`' && !source.startsWith('${', i)) {
|
||||
if (source[i] === '\n') line += 1;
|
||||
i += 1;
|
||||
}
|
||||
if (i >= source.length) fail('a template literal that does not end');
|
||||
push('string', '`');
|
||||
if (source[i] === '`') {
|
||||
i += 1;
|
||||
} else {
|
||||
i += 2;
|
||||
braces.push('template');
|
||||
}
|
||||
};
|
||||
while (i < source.length) {
|
||||
const ch = source[i];
|
||||
if (ch === '\n') {
|
||||
line += 1;
|
||||
i += 1;
|
||||
} else if (ch === ' ') {
|
||||
i += 1;
|
||||
} else if (source.startsWith('//', i)) {
|
||||
while (i < source.length && source[i] !== '\n') i += 1;
|
||||
} else if (source.startsWith('/*', i)) {
|
||||
const end = source.indexOf('*/', i + 2);
|
||||
if (end < 0) fail('a comment that does not end');
|
||||
line += source.slice(i, end).split('\n').length - 1;
|
||||
i = end + 2;
|
||||
} else if (ch === "'" || ch === '"') {
|
||||
const end = source.indexOf(ch, i + 1);
|
||||
if (end < 0 || source.slice(i, end).includes('\n')) fail('a string that does not end');
|
||||
push('string', source.slice(i + 1, end));
|
||||
i = end + 1;
|
||||
} else if (ch === '`') {
|
||||
i += 1;
|
||||
template();
|
||||
} else if (/[A-Za-z_$]/.test(ch)) {
|
||||
const name = /^[A-Za-z_$][\w$]*/.exec(source.slice(i))[0];
|
||||
push('name', name);
|
||||
i += name.length;
|
||||
} else if (/[0-9]/.test(ch)) {
|
||||
const number = /^(0x[0-9a-fA-F]+|[0-9]+)/.exec(source.slice(i))[0];
|
||||
push('number', number);
|
||||
i += number.length;
|
||||
} else if (ch === '}' && braces.at(-1) === 'template') {
|
||||
braces.pop();
|
||||
i += 1;
|
||||
template();
|
||||
} else {
|
||||
const punct = PUNCTUATORS.find((p) => source.startsWith(p, i));
|
||||
if (!punct) fail(`the character ${JSON.stringify(ch)}`);
|
||||
if (punct === '/') {
|
||||
const before = tokens.at(-1);
|
||||
if (!before || BEFORE_EXPRESSION.has(before.value)) fail('a regular expression');
|
||||
}
|
||||
if (punct === '{') braces.push('brace');
|
||||
if (punct === '}') braces.pop();
|
||||
push('punct', punct);
|
||||
i += punct.length;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// The index of the ) that closes the ( at `open`.
|
||||
function closing(tokens, open) {
|
||||
let depth = 0;
|
||||
for (let i = open; i < tokens.length; i += 1) {
|
||||
if (tokens[i].value === '(') depth += 1;
|
||||
if (tokens[i].value === ')') depth -= 1;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
return tokens.length;
|
||||
}
|
||||
|
||||
// The names the module declares: consts, lets, functions, imports and the
|
||||
// parameters of its functions, arrows and methods.
|
||||
function declaredNames(tokens) {
|
||||
const names = new Set();
|
||||
tokens.forEach((token, i) => {
|
||||
const before = tokens[i - 1];
|
||||
if (token.kind !== 'name') return;
|
||||
if (before && ['const', 'let', 'function'].includes(before.value)) names.add(token.value);
|
||||
});
|
||||
let inImport = false;
|
||||
for (const token of tokens) {
|
||||
if (token.value === 'import') inImport = true;
|
||||
else if (token.value === 'from') inImport = false;
|
||||
else if (inImport && token.kind === 'name' && token.value !== 'type') names.add(token.value);
|
||||
}
|
||||
tokens.forEach((token, open) => {
|
||||
if (token.value !== '(') return;
|
||||
const end = closing(tokens, open);
|
||||
if (!['=>', '{', ':'].includes(tokens[end + 1]?.value)) return;
|
||||
for (let i = open + 1; i < end; i += 1) {
|
||||
const isParameter =
|
||||
tokens[i].kind === 'name' &&
|
||||
['(', ',', '{'].includes(tokens[i - 1].value) &&
|
||||
[',', ')', ':', '}', '='].includes(tokens[i + 1].value);
|
||||
if (isParameter) names.add(tokens[i].value);
|
||||
}
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
// What the tokens hold that a dialect may not; empty when nothing.
|
||||
export function tokenProblems(tokens) {
|
||||
const problems = [];
|
||||
const declared = declaredNames(tokens);
|
||||
const isLocal = (i) => tokens[i]?.kind === 'name' && tokens[i - 1]?.value !== '.';
|
||||
tokens.forEach((token, i) => {
|
||||
const before = tokens[i - 1];
|
||||
const after = tokens[i + 1];
|
||||
const at = `line ${token.line}`;
|
||||
if (token.kind === 'name') {
|
||||
if (before?.value === '.') {
|
||||
if (!PROPERTIES.has(token.value)) problems.push(`${at} reads .${token.value}, which a dialect does not`);
|
||||
return;
|
||||
}
|
||||
// A key of an object literal or a parameter with its type.
|
||||
if (after?.value === ':') return;
|
||||
// A method of an object literal: name(params) { ... }.
|
||||
if (
|
||||
after?.value === '(' &&
|
||||
['{', ','].includes(before?.value) &&
|
||||
tokens[closing(tokens, i + 1) + 1]?.value === '{'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!KEYWORDS.has(token.value) && !GLOBALS.has(token.value) && !declared.has(token.value)) {
|
||||
problems.push(`${at} names ${token.value}, which it neither declares nor may use`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (token.kind !== 'punct') return;
|
||||
if (ASSIGNMENTS.has(token.value)) {
|
||||
// const out: number[] = [] has a type before the =, not a property.
|
||||
const typed = before?.value === ']' && tokens[i - 2]?.value === '[';
|
||||
if (!typed && !isLocal(i - 1)) problems.push(`${at} assigns to something other than a name of its own`);
|
||||
} else if (token.value === '++' || token.value === '--') {
|
||||
const postfix = before && (before.kind === 'name' || [')', ']'].includes(before.value));
|
||||
const target = postfix ? isLocal(i - 1) : isLocal(i + 1) && tokens[i + 2]?.value !== '.';
|
||||
if (!target) problems.push(`${at} counts something other than a name of its own`);
|
||||
} else if (token.value === '[') {
|
||||
const reads = before && (before.kind !== 'punct' || [')', ']'].includes(before.value));
|
||||
if (reads && after?.value !== ']') problems.push(`${at} reads a property by a computed name`);
|
||||
}
|
||||
});
|
||||
return [...new Set(problems)];
|
||||
}
|
||||
|
||||
// What is wrong with the source before it runs; empty when nothing is.
|
||||
export function staticProblems(source, version) {
|
||||
const problems = [];
|
||||
@@ -87,6 +378,7 @@ export function staticProblems(source, version) {
|
||||
if (!source.startsWith('/**\n')) problems.push('it does not start with its header comment');
|
||||
if (source.includes('\\')) problems.push('it holds a backslash');
|
||||
if (source.includes('\r')) problems.push('it holds a carriage return');
|
||||
if (source.includes('\t')) problems.push('it holds a tab');
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!/^\s*(import|export\s+\*|export\s*\{)/.test(line) && !/\bfrom\s+'/.test(line)) continue;
|
||||
const match = IMPORT_LINE.exec(line);
|
||||
@@ -97,8 +389,11 @@ export function staticProblems(source, version) {
|
||||
const line = lines.findIndex((text) => pattern.test(text));
|
||||
if (line >= 0) problems.push(`line ${line + 1} names what a dialect does not need (${pattern.source})`);
|
||||
}
|
||||
const computed = lines.findIndex((text) => COMPUTED_ACCESS.test(text));
|
||||
if (computed >= 0) problems.push(`line ${computed + 1} reads a property by a computed name`);
|
||||
try {
|
||||
problems.push(...tokenProblems(tokenize(source)));
|
||||
} catch (err) {
|
||||
problems.push(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
if (!new RegExp(`^ releasedFor: '${escapeRegExp(version)}',$`, 'm').test(source)) {
|
||||
problems.push(`it is not written for ${version}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { checkDialect, staticProblems } from './check-registration-dialect.mjs';
|
||||
import { checkDialect, staticProblems, tokenize, tokenProblems } from './check-registration-dialect.mjs';
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
@@ -7,11 +7,12 @@ import { join } from 'node:path';
|
||||
import { after, describe, it } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// The issuer's sample: every building block a generated dialect can use, for version 9.9.9.
|
||||
const SAMPLE = fileURLToPath(
|
||||
new URL('../../server/tests/fixtures/service-identity/registration-dialect.sample.ts', import.meta.url),
|
||||
// A dialect written by hand for TREK's tests, for version 9.9.9. It is none of
|
||||
// the issuer's; the issuer checks its own modules against these rules too.
|
||||
const EXAMPLE = fileURLToPath(
|
||||
new URL('../../server/tests/fixtures/service-identity/registration-dialect.example.ts', import.meta.url),
|
||||
);
|
||||
const source = readFileSync(SAMPLE, 'utf8');
|
||||
const source = readFileSync(EXAMPLE, 'utf8');
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'registration-dialect-check-'));
|
||||
after(() => rmSync(scratch, { recursive: true, force: true }));
|
||||
@@ -25,17 +26,20 @@ function variant(text) {
|
||||
}
|
||||
|
||||
function replaced(from, to) {
|
||||
assert.ok(source.includes(from), `the sample no longer holds ${from}`);
|
||||
assert.ok(source.includes(from), `the example no longer holds ${from}`);
|
||||
return source.replace(from, to);
|
||||
}
|
||||
|
||||
// The example with `line` added in front of its exports.
|
||||
const withLine = (line) => replaced('export const registrationDialect', `${line}\nexport const registrationDialect`);
|
||||
|
||||
describe('check-registration-dialect', () => {
|
||||
it('passes the issuer sample for its own version', async () => {
|
||||
assert.deepEqual(await checkDialect(SAMPLE, '9.9.9'), []);
|
||||
it('passes the example for its own version', async () => {
|
||||
assert.deepEqual(await checkDialect(EXAMPLE, '9.9.9'), []);
|
||||
});
|
||||
|
||||
it('refuses a dialect written for another version', async () => {
|
||||
const problems = await checkDialect(SAMPLE, '9.9.10');
|
||||
const problems = await checkDialect(EXAMPLE, '9.9.10');
|
||||
assert.ok(problems.some((p) => p.includes('not written for 9.9.10')));
|
||||
});
|
||||
|
||||
@@ -51,7 +55,7 @@ describe('check-registration-dialect', () => {
|
||||
|
||||
it('refuses what reaches the process, the network or code from strings, before running any of it', async () => {
|
||||
for (const line of ['const env = process.env;', 'void fetch("https://example.org");', 'const run = eval;']) {
|
||||
const file = variant(replaced('const DIALECT =', `${line}\nconst DIALECT =`));
|
||||
const file = variant(withLine(line));
|
||||
const problems = await checkDialect(file, '9.9.9');
|
||||
assert.ok(
|
||||
problems.some((p) => p.includes('names what a dialect does not need')),
|
||||
@@ -60,27 +64,73 @@ describe('check-registration-dialect', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses a computed property name and any escape that could spell one', () => {
|
||||
const computed = replaced('const DIALECT =', "const name = BASE32['length'];\nconst DIALECT =");
|
||||
it('refuses changing a global or anything else that is not its own name', () => {
|
||||
for (const line of [
|
||||
'Math.random = () => 0;',
|
||||
'Buffer.from = Buffer.alloc;',
|
||||
'const m = Math;\nm.floor = Math.max;',
|
||||
'const b = Buffer;\nb.poolSize += 1;',
|
||||
'(Buffer).concat = Buffer.alloc;',
|
||||
'PEPPER.c++;',
|
||||
'++PEPPER.c;',
|
||||
]) {
|
||||
const problems = staticProblems(withLine(line), '9.9.9');
|
||||
assert.ok(
|
||||
problems.some((p) => p.includes('other than a name of its own') || p.includes('reads .')),
|
||||
line,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses names and members it does not know, however they are reached', () => {
|
||||
const cases = [
|
||||
['Object.defineProperty(JSON, "parse", {});', 'names what a dialect does not need'],
|
||||
['const pick = Math.random();', 'reads .random'],
|
||||
['const big = Buffer.alloc(4).buffer;', 'reads .buffer'],
|
||||
['const own = Array.isArray;', 'reads .isArray'],
|
||||
['const later = Promise.resolve(1);', 'names Promise'],
|
||||
['const date = new Date();', 'names new'],
|
||||
['delete PEPPER.c;', 'names delete'],
|
||||
['const kind = typeof PEPPER;', 'names typeof'],
|
||||
['const at = PEPPER?.length;', 'the character'],
|
||||
];
|
||||
for (const [line, expected] of cases) {
|
||||
const problems = staticProblems(withLine(line), '9.9.9');
|
||||
assert.ok(
|
||||
problems.some((p) => p.includes(expected)),
|
||||
`${line}: ${problems.join('; ')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses a computed property name, a regular expression and any escape that could spell a name', () => {
|
||||
const computed = withLine("const name = PEPPER['length'];");
|
||||
assert.ok(staticProblems(computed, '9.9.9').some((p) => p.includes('computed name')));
|
||||
const escaped = replaced('const DIALECT =', "const name = 'pro\\u0063ess';\nconst DIALECT =");
|
||||
const pattern = withLine('const test = /process/.source;');
|
||||
assert.ok(staticProblems(pattern, '9.9.9').some((p) => p.includes('regular expression')));
|
||||
const escaped = withLine("const name = 'pro\\u0063ess';");
|
||||
assert.ok(staticProblems(escaped, '9.9.9').some((p) => p.includes('backslash')));
|
||||
});
|
||||
|
||||
it('reads the names in a template literal like any other', () => {
|
||||
assert.deepEqual(tokenProblems(tokenize('const a = 1;\nconst b = `x${a}y`;\n')), []);
|
||||
assert.ok(tokenProblems(tokenize('const b = `x${Date}y`;\n')).some((p) => p.includes('names Date')));
|
||||
});
|
||||
|
||||
it('refuses a file without its header or without a dialect id', () => {
|
||||
assert.ok(staticProblems(source.slice(source.indexOf('import')), '9.9.9').some((p) => p.includes('header')));
|
||||
const noId = replaced("const DIALECT = 'KitchenSink0';", "const DIALECT = 'short';");
|
||||
const noId = replaced("const DIALECT = 'HandWritten1';", "const DIALECT = 'short';");
|
||||
assert.ok(staticProblems(noId, '9.9.9').some((p) => p.includes('dialect id')));
|
||||
});
|
||||
|
||||
it('refuses a dialect that does not compute what its SELF_CHECK says', async () => {
|
||||
const file = variant(replaced("seal11.toString('base64url')", "seal11.toString('hex')"));
|
||||
const file = variant(replaced("seal.toString('hex')", "seal.toString('base64url')"));
|
||||
const problems = await checkDialect(file, '9.9.9');
|
||||
assert.deepEqual(problems, ['its request is not the one the issuer computed']);
|
||||
});
|
||||
|
||||
it('refuses a dialect whose request TREK would not send', async () => {
|
||||
const file = variant(replaced("'X-TREK-Ferry-Seal'", "'X-TREK-Install'"));
|
||||
const file = variant(replaced("'X-TREK-Test-Seal'", "'X-TREK-Install'"));
|
||||
const problems = await checkDialect(file, '9.9.9');
|
||||
assert.ok(problems.some((p) => p.includes('X-TREK-Install')));
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# by check:entities, so Prettier must not rewrite them.
|
||||
src/db/entities/
|
||||
|
||||
# Written by the TREK services issuer and committed as it wrote them: the
|
||||
# registration dialect (taken over by the release job) and the issuer's sample.
|
||||
# Written by the TREK services issuer and committed as it wrote it: the
|
||||
# registration dialect, taken over by the release job.
|
||||
src/nest/service-identity/registration-dialect.ts
|
||||
tests/fixtures/service-identity/registration-dialect.sample.ts
|
||||
|
||||
@@ -34,13 +34,9 @@ const root = fileURLToPath(new URL('..', import.meta.url));
|
||||
// byte to its output by check:entities, so their layout is the generator's, not
|
||||
// Prettier's. .prettierignore keeps `npm run format` off them for the same reason.
|
||||
const GENERATED = 'src/db/entities/';
|
||||
// The registration dialect and the issuer's sample of one are written by the
|
||||
// TREK services issuer and committed as it wrote them (the release job takes
|
||||
// the dialect over unchanged), so their layout is the issuer's generator's.
|
||||
const ISSUER_WRITTEN = [
|
||||
'src/nest/service-identity/registration-dialect.ts',
|
||||
'tests/fixtures/service-identity/registration-dialect.sample.ts',
|
||||
];
|
||||
// The registration dialect is written by the TREK services issuer and taken
|
||||
// over unchanged by the release job, so its layout is the issuer's generator's.
|
||||
const ISSUER_WRITTEN = ['src/nest/service-identity/registration-dialect.ts'];
|
||||
await runCli('lint:format', (args) =>
|
||||
check({
|
||||
prettier,
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
* such release it speaks the original registration, which the issuer accepts
|
||||
* for a transition period. Renewing a key never goes through this file: an
|
||||
* installation keeps its key whatever release it runs. After a stable release,
|
||||
* merge main into dev so that dev and prerelease builds register with the
|
||||
* current dialect.
|
||||
*
|
||||
* The dialect is the registration's, not SQL's; lint:dialect has nothing to
|
||||
* look for here.
|
||||
* merge main into dev so that dev builds register with the current dialect.
|
||||
*/
|
||||
import type { DialectSelfCheck, RegistrationDialect } from './registration-dialect.types';
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* A registration dialect written by hand for TREK's tests. It has the shape of
|
||||
* the modules the issuer writes for each release (the exports, the paths, a
|
||||
* tag in a header, helpers with loops), so the release check and the key
|
||||
* service can be tested against a dialect that is not the committed one, but
|
||||
* it is none of the issuer's and shows nothing of how they are made.
|
||||
*/
|
||||
import type { DialectSelfCheck, RegistrationDialect } from './registration-dialect.types';
|
||||
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
import { crc32 } from 'node:zlib';
|
||||
|
||||
const DIALECT = 'HandWritten1';
|
||||
const PEPPER = Buffer.from('7472656b2d746573742d6469616c656374', 'hex');
|
||||
|
||||
function withChecksum(input: Buffer): Buffer {
|
||||
const out = Buffer.alloc(input.length + 4);
|
||||
input.copy(out);
|
||||
out.writeUInt32BE(crc32(input), input.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
function backwards(input: Buffer): Buffer {
|
||||
const out: number[] = [];
|
||||
for (let i = input.length - 1; i >= 0; i--) out.push(input.readUInt8(i) ^ (i & 0xff));
|
||||
return Buffer.from(out);
|
||||
}
|
||||
|
||||
export const registrationDialect: RegistrationDialect = {
|
||||
id: DIALECT,
|
||||
releasedFor: '9.9.9',
|
||||
createdAt: '2026-10-29',
|
||||
challengePath: `/v1/register/${DIALECT}/challenge`,
|
||||
registerPath: `/v1/register/${DIALECT}`,
|
||||
begin({ challenge, version, random, nowSeconds }) {
|
||||
const c = Buffer.from(challenge.c, 'utf8');
|
||||
const salt = createHmac('sha256', PEPPER).update(c).digest();
|
||||
return {
|
||||
work(nonce) {
|
||||
const joined = Buffer.concat([c, Buffer.from(`.${nonce}.`), Buffer.from(version, 'utf8'), random]);
|
||||
return { password: backwards(withChecksum(createHash('sha512').update(joined).digest())), salt };
|
||||
},
|
||||
request(nonce) {
|
||||
const seal = createHmac('sha256', random).update(`${challenge.c}|${nonce}|${nowSeconds}`).digest();
|
||||
return {
|
||||
body: {
|
||||
ticket: challenge.c,
|
||||
count: String(nonce),
|
||||
release: version,
|
||||
lantern: random.toString('base64url'),
|
||||
sentAt: nowSeconds,
|
||||
},
|
||||
headers: { 'X-TREK-Test-Seal': seal.toString('hex') },
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/** Fixed inputs and what this module makes of them. */
|
||||
export const SELF_CHECK: DialectSelfCheck = {
|
||||
challenge: { c: 'v1.selfcheck.HandWritten1', alg: 'scrypt', N: 16, r: 1, p: 1, bits: 0, expires_at: 1 },
|
||||
version: '9.9.9',
|
||||
random: '000102030405060708090a0b0c0d0e0f',
|
||||
nowSeconds: 1800000000,
|
||||
nonce: 41,
|
||||
passwordSha256: 'cf7d2be18b531a1a1f2427ce744cd1bd7b18ae731e1449ac56c827e7a683da51',
|
||||
saltSha256: '005cc836f9d0a082e0c3712401f1d95515b6066855e8f10533353b6eb0e76f3d',
|
||||
scrypt: '5fd715b7942b9bc9ccee9b811b5566d31aa7ca6d3a011222fe3101d6178d4396',
|
||||
requestSha256: '2b41dc94a997e22cecc7bb946889208fc86c8dff0f5e30075a2fcee28239f0b8',
|
||||
};
|
||||
@@ -1,262 +0,0 @@
|
||||
/**
|
||||
* How TREK 9.9.9 registers with the TREK services.
|
||||
*
|
||||
* The issuer writes this file for each release and the release pipeline
|
||||
* commits it with the version bump. It is not edited by hand, and its layout
|
||||
* is the generator's, so Prettier leaves it alone. Renewing a key never goes
|
||||
* through it: an installation keeps its key whatever release it runs. After a
|
||||
* stable release, merge main into dev so that dev and prerelease builds
|
||||
* register with the current dialect.
|
||||
*
|
||||
* The dialect is the registration's, not SQL's; lint:dialect has nothing to
|
||||
* look for here.
|
||||
*/
|
||||
import type { DialectSelfCheck, RegistrationDialect } from './registration-dialect.types';
|
||||
|
||||
import { createHash, createHmac, hkdfSync, pbkdf2Sync } from 'node:crypto';
|
||||
import { crc32 } from 'node:zlib';
|
||||
|
||||
const DIALECT = 'KitchenSink0';
|
||||
const CONSTANT = Buffer.from('206ebd4ad98b5f9c3531195aa89f6285612c396a98999a127b7972b6eefe03a8', 'hex');
|
||||
|
||||
const BASE32 = 'abcdefghijklmnopqrstuvwxyz234567';
|
||||
|
||||
function u32be(value: number): Buffer {
|
||||
const out = Buffer.alloc(4);
|
||||
out.writeUInt32BE(value >>> 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
function u32le(value: number): Buffer {
|
||||
const out = Buffer.alloc(4);
|
||||
out.writeUInt32LE(value >>> 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
function u64be(value: number): Buffer {
|
||||
const out = Buffer.alloc(8);
|
||||
out.writeBigUInt64BE(BigInt(value));
|
||||
return out;
|
||||
}
|
||||
|
||||
function u64le(value: number): Buffer {
|
||||
const out = Buffer.alloc(8);
|
||||
out.writeBigUInt64LE(BigInt(value));
|
||||
return out;
|
||||
}
|
||||
|
||||
function leb128(value: number): Buffer {
|
||||
const out: number[] = [];
|
||||
let rest = value;
|
||||
do {
|
||||
const low = rest % 128;
|
||||
rest = Math.floor(rest / 128);
|
||||
out.push(low | (Math.sign(rest) << 7));
|
||||
} while (rest > 0);
|
||||
return Buffer.from(out);
|
||||
}
|
||||
|
||||
function lp32le(part: Buffer): Buffer {
|
||||
const head = Buffer.alloc(4);
|
||||
head.writeUInt32LE(part.length);
|
||||
return Buffer.concat([head, part]);
|
||||
}
|
||||
|
||||
function base32(input: Buffer): string {
|
||||
const bits = [...input].map((byte) => byte.toString(2).padStart(8, '0')).join('');
|
||||
let out = '';
|
||||
for (let i = 0; i < bits.length; i += 5) {
|
||||
out += BASE32.charAt(parseInt(bits.slice(i, i + 5).padEnd(5, '0'), 2));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function reverseBlocks(input: Buffer, width: number): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
for (let i = 0; i + width <= out.length; i += width) out.subarray(i, i + width).reverse();
|
||||
return out;
|
||||
}
|
||||
|
||||
function permute(input: Buffer, order: number[]): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
order.forEach((from, to) => out.writeUInt8(input.readUInt8(from), to));
|
||||
return out;
|
||||
}
|
||||
|
||||
function fold(input: Buffer, size: number): Buffer {
|
||||
const out = Buffer.alloc(size);
|
||||
input.forEach((byte, i) => out.writeUInt8(out.readUInt8(i % size) ^ byte, i % size));
|
||||
return out;
|
||||
}
|
||||
|
||||
function interleave(a: Buffer, b: Buffer): Buffer {
|
||||
const parts: Buffer[] = [];
|
||||
for (let i = 0; i < Math.max(a.length, b.length); i++) parts.push(a.subarray(i, i + 1), b.subarray(i, i + 1));
|
||||
return Buffer.concat(parts);
|
||||
}
|
||||
|
||||
function mixWordsBe(input: Buffer, rotation: number, consts: Buffer): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
let previous = 0;
|
||||
for (let i = 0; i + 4 <= out.length; i += 4) {
|
||||
const word = (out.readUInt32BE(i) + consts.readUInt32BE(i % consts.length)) >>> 0;
|
||||
previous = (((word << rotation) | (word >>> (32 - rotation))) ^ previous) >>> 0;
|
||||
out.writeUInt32BE(previous, i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function mixWordsLe(input: Buffer, rotation: number, consts: Buffer): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
let previous = 0;
|
||||
for (let i = 0; i + 4 <= out.length; i += 4) {
|
||||
const word = (out.readUInt32LE(i) + consts.readUInt32BE(i % consts.length)) >>> 0;
|
||||
previous = (((word << rotation) | (word >>> (32 - rotation))) ^ previous) >>> 0;
|
||||
out.writeUInt32LE(previous, i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function keystream8be(input: Buffer, start: number): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
for (let offset = 0; offset < out.length; offset += 32) {
|
||||
const counter = Buffer.from([(start + offset / 32) & 0xff]);
|
||||
const block = createHash('sha256').update(CONSTANT).update(counter).digest();
|
||||
for (let j = offset; j < Math.min(offset + 32, out.length); j++) {
|
||||
out.writeUInt8(out.readUInt8(j) ^ block.readUInt8(j - offset), j);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function keystream16le(input: Buffer, start: number): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
for (let offset = 0; offset < out.length; offset += 32) {
|
||||
const counter = Buffer.alloc(2);
|
||||
counter.writeUInt16LE((start + offset / 32) & 0xffff);
|
||||
const block = createHash('sha256').update(CONSTANT).update(counter).digest();
|
||||
for (let j = offset; j < Math.min(offset + 32, out.length); j++) {
|
||||
out.writeUInt8(out.readUInt8(j) ^ block.readUInt8(j - offset), j);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function keystream32be(input: Buffer, start: number): Buffer {
|
||||
const out = Buffer.from(input);
|
||||
for (let offset = 0; offset < out.length; offset += 32) {
|
||||
const counter = Buffer.alloc(4);
|
||||
counter.writeUInt32BE((start + offset / 32) >>> 0);
|
||||
const block = createHash('sha256').update(CONSTANT).update(counter).digest();
|
||||
for (let j = offset; j < Math.min(offset + 32, out.length); j++) {
|
||||
out.writeUInt8(out.readUInt8(j) ^ block.readUInt8(j - offset), j);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export const registrationDialect: RegistrationDialect = {
|
||||
id: DIALECT,
|
||||
releasedFor: '9.9.9',
|
||||
createdAt: '2026-01-01',
|
||||
challengePath: `/v1/register/${DIALECT}/challenge`,
|
||||
registerPath: `/v1/register/${DIALECT}`,
|
||||
begin({ challenge, version, random, nowSeconds }) {
|
||||
const c = Buffer.from(challenge.c, 'utf8');
|
||||
const v = Buffer.from(version, 'utf8');
|
||||
const hc = createHash('sha256').update(c).digest();
|
||||
const salt1 = Buffer.concat([
|
||||
CONSTANT.subarray(0, 8),
|
||||
Buffer.from([0x1f]),
|
||||
hc,
|
||||
Buffer.from([0x1f]),
|
||||
c.subarray(3, 19),
|
||||
]);
|
||||
const salt2 = createHash('shake256', { outputLength: 24 }).update(salt1).digest();
|
||||
const salt3 = pbkdf2Sync(salt2, random, 3, 32, 'sha512');
|
||||
const salt4 = Buffer.from(hkdfSync('sha384', salt3, hc.subarray(0, 16), Buffer.from('a1b2c3d4e5', 'hex'), 40));
|
||||
const salt5 = createHmac('sha3-256', CONSTANT).update(salt4).digest();
|
||||
const salt6 = fold(salt5, 20);
|
||||
const salt7Words = Buffer.from('deadbeef0badf00d', 'hex');
|
||||
const salt7 = mixWordsBe(salt6, 5, salt7Words);
|
||||
const salt8 = Buffer.concat([random.subarray(2, 9), salt7]);
|
||||
const salt9 = Buffer.from(salt8.toString('base64url'), 'utf8');
|
||||
return {
|
||||
work(nonce) {
|
||||
const pass1 = Buffer.concat([
|
||||
lp32le(c),
|
||||
lp32le(leb128(nonce)),
|
||||
lp32le(v),
|
||||
lp32le(random),
|
||||
lp32le(u32le(nowSeconds)),
|
||||
lp32le(u64le(nonce)),
|
||||
lp32le(hc.subarray(3, 19)),
|
||||
]);
|
||||
const pass2 = createHash('blake2b512').update(pass1).digest();
|
||||
const pass3Words = Buffer.from('9e3779b97f4a7c1501234567', 'hex');
|
||||
const pass3 = mixWordsLe(mixWordsLe(pass2, 7, pass3Words), 13, pass3Words);
|
||||
const pass4 = keystream16le(pass3, 5);
|
||||
const pass5 = Buffer.concat([pass4.subarray(9 % pass4.length), pass4.subarray(0, 9 % pass4.length)]);
|
||||
const pass6 = reverseBlocks(pass5, 4);
|
||||
const pass7 = permute(pass6, [3, 0, 15, 7, 1, 2, 4, 6, 5, 8, 9, 11, 10, 13, 12, 14]);
|
||||
const pass8 = Buffer.from(pass7.map((byte) => (byte * 37 + 11) & 0xff));
|
||||
const pass9 = interleave(pass8, CONSTANT);
|
||||
const pass10 = Buffer.concat([pass9, u32be(crc32(pass9))]);
|
||||
const pass11 = Buffer.from(pass10.toString('hex'), 'utf8');
|
||||
const pass12 = Buffer.concat([pass11, Buffer.from([0x3a]), v]);
|
||||
const pass13 = Buffer.from(base32(pass12), 'utf8');
|
||||
const pass14 = pass13.subarray(10, 58);
|
||||
return { password: pass14, salt: salt9 };
|
||||
},
|
||||
request(nonce) {
|
||||
const seal1 = Buffer.concat([
|
||||
v,
|
||||
Buffer.from(String(nonce), 'utf8'),
|
||||
Buffer.from(String(nowSeconds), 'utf8'),
|
||||
c,
|
||||
random,
|
||||
u64be(nonce),
|
||||
u32be(nowSeconds),
|
||||
Buffer.from(nonce.toString(16), 'utf8'),
|
||||
]);
|
||||
const seal2 = createHmac('sha512', seal1).update(CONSTANT).digest();
|
||||
const seal3 = Buffer.from(seal2.toString('base64'), 'utf8');
|
||||
const seal4 = createHash('sha384').update(seal3).digest();
|
||||
const seal5 = Buffer.from(seal4.toString('hex').toUpperCase(), 'utf8');
|
||||
const seal6 = Buffer.from(seal5).reverse();
|
||||
const seal7 = keystream32be(seal6, 200);
|
||||
const seal8 = interleave(seal7, hc).subarray(0, seal7.length);
|
||||
const seal9 = Buffer.concat([seal8, u32le(crc32(seal8))]);
|
||||
const seal10 = keystream8be(seal9, 1);
|
||||
const seal11 = seal10.subarray(8, 40);
|
||||
return {
|
||||
body: {
|
||||
'ferry-passage': {
|
||||
'release-mark': version,
|
||||
tide: 'amber-41',
|
||||
'ticket-stamp': challenge.c,
|
||||
harbor: nonce.toString(16),
|
||||
'gull-wing': 'slate-7',
|
||||
lantern: base32(random),
|
||||
compass: nowSeconds,
|
||||
},
|
||||
},
|
||||
headers: { 'X-TREK-Ferry-Seal': seal11.toString('base64url') },
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/** Fixed inputs and what this module must make of them; TREK's tests and release job run it. */
|
||||
export const SELF_CHECK: DialectSelfCheck = {
|
||||
challenge: { c: 'v1.selfcheck.KitchenSink0', alg: 'scrypt', N: 16, r: 1, p: 1, bits: 0, expires_at: 1 },
|
||||
version: '9.9.9',
|
||||
random: '000102030405060708090a0b0c0d0e0f',
|
||||
nowSeconds: 1800000000,
|
||||
nonce: 41,
|
||||
passwordSha256: '7b2c79ba69f022e031c4db19ff0bdf1f67bed98498a73ed747f968d346bc3110',
|
||||
saltSha256: '552659da096a9c393d520721f1aa26e5d3716305f4a6167effb13b9e7660e737',
|
||||
scrypt: '611e71acba947b2fffb86a831d4f389c8f72f5182d7bde30d4bb1f5c3abac730',
|
||||
requestSha256: '3df6e9c6fb70e039a2cd072cb526e975c996e6e6618ded4c89e09f6a7440c4f4',
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* The sample dialect beside this file is the issuer's own test output, kept
|
||||
* byte for byte, and imports its types from next to itself the way a real
|
||||
* dialect does in src/nest/service-identity/. This points that import at the
|
||||
* real types, so the sample is typechecked and linted against them.
|
||||
* The example dialect beside this file imports its types from next to itself
|
||||
* the way a real dialect does in src/nest/service-identity/, so the release
|
||||
* check reads it as it reads one. This points that import at the real types,
|
||||
* so the example is typechecked and linted against them.
|
||||
*/
|
||||
export type {
|
||||
DialectSelfCheck,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The registration dialect: the module this build carries, and the issuer's
|
||||
* sample that uses every building block a generated dialect can be made of.
|
||||
* The registration dialect: the module this build carries, and a dialect
|
||||
* written by hand in the shape of the issuer's (a tag in a header, helpers).
|
||||
* Each must compute what the issuer computed for its fixed inputs (SELF_CHECK),
|
||||
* and its requests must be ones the issuer client lets out, so a dialect that
|
||||
* reaches main is one TREK can run.
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
RegistrationDialect,
|
||||
} from '../../../../src/nest/service-identity/registration-dialect.types';
|
||||
import { leadingZeroBits, powHash, solvePow } from '../../../../src/nest/service-identity/service-key.pow';
|
||||
import * as sample from '../../../fixtures/service-identity/registration-dialect.sample';
|
||||
import * as example from '../../../fixtures/service-identity/registration-dialect.example';
|
||||
|
||||
import { createHash, scryptSync } from 'node:crypto';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -42,7 +42,7 @@ function inputsOf(check: DialectSelfCheck): DialectInputs {
|
||||
|
||||
const modules: [string, { registrationDialect: RegistrationDialect; SELF_CHECK: DialectSelfCheck }][] = [
|
||||
['the committed dialect', committed],
|
||||
["the issuer's sample", sample],
|
||||
['a hand-written dialect', example],
|
||||
];
|
||||
|
||||
describe.each(modules)('%s', (_, { registrationDialect: dialect, SELF_CHECK: check }) => {
|
||||
|
||||
Reference in New Issue
Block a user