Skip to content

chore: move the scaffold onto the current ecosystem and offer Fastify - #146

Merged
Bccorb merged 12 commits into
mainfrom
chore/ecosystem-v0-6-0
Aug 2, 2026
Merged

chore: move the scaffold onto the current ecosystem and offer Fastify#146
Bccorb merged 12 commits into
mainfrom
chore/ecosystem-v0-6-0

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Bumps the pinned ecosystem so seamless init scaffolds from the current releases, and adds Fastify as a backend option.

Pin Was Now
SEAMLESS_AUTH_API_VERSION v0.5.0 v0.7.1
SEAMLESS_AUTH_ADMIN_DASHBOARD_VERSION v0.3.0 v0.4.0
SEAMLESS_TEMPLATES_REF v0.5.0 v0.8.1
@seamless-auth/express (conformance adapter) ^0.9.0 ^0.12.0

Templates v0.8.1 carries @seamless-auth/react ^0.8.0 in both React starters, @seamless-auth/express ^0.12.0 in the Express starter, and @seamless-auth/fastify ^0.3.1 in the Fastify starter.

Registration without a passkey

Registration used to end on a screen with one control. A user who did not want a passkey, or whose device could not make one, had no way forward. The starters now offer a skip when the instance has another login method enabled, and say so plainly when it does not.

That path only works if the whole chain moves together, which is why this bumps them as a set:

  • The auth API adds an unauthenticated GET /system-config/public returning the configured loginMethods and nothing else (v0.7.0).
  • The adapters serve routes from an explicit list, so they had to add the proxy (@seamless-auth/express 0.12.0, @seamless-auth/fastify 0.3.0).
  • The React starters read it to gate the skip control (@seamless-auth/react 0.8.0).

A web template upgraded on its own would read nothing and show no skip at all. This also fixes a real mismatch: the React SDK previously fell back to a hardcoded ['passkey', 'magic_link', 'phone_otp'], so a client could advertise a method the instance had turned off.

Fastify registration fix

Registering against a scaffolded Fastify API returned a 500 with TypeError: option maxAge is invalid: 300. The auth server sent the registration response's ttl as the string "300", and the Fastify adapter handed it to a cookie library whose Number.isInteger check rejects a string. The Express starter never showed this, because its adapter multiplies the value into milliseconds and so coerced it on the way past.

Fixed from both ends: @seamless-auth/core 0.12.1 parses the lifetime once, before it reaches an adapter, and throws on anything that is not a positive whole number of seconds rather than issuing a cookie with a lifetime nobody can vouch for; auth API v0.7.1 sends a number. Templates v0.8.1 carries it through the Fastify starter's committed lockfile, which is what pinned the broken version.

The conformance adapter needed no pin change. It has no lockfile, so ^0.12.0 already resolves to the fixed 0.12.1.

Fastify

seamless init offers a second backend, listed as "Fastify (beta)" beside Express. It serves the same surface as the Express starter on an identical env contract, including the admin console at /console behind SERVE_ADMIN_CONSOLE.

This needed no CLI code. The backend prompt builds its options from the registry and already renders a beta status as a (beta) suffix without disabling the entry, and requires.cliMin (0.10.0) is satisfied by 0.10.2. Verified through the real code path at the new ref:

api options as the prompt renders them:
  - Express  (id=express)
  - Fastify (beta)  (id=fastify)

Two boot-time fixes land with v0.8.0: an empty PORT= in .env now falls back to 3000 rather than binding a random free port, and pino-pretty moves to a runtime dependency so an install without dev dependencies boots.

What else users get

Both React starters gain a protected /session route showing the issued claims, roles, organization context, step-up freshness, and registered passkeys, so the first authenticated screen reads as an app rather than a JSON.stringify dump. Missing configuration now stops a scaffolded project with a message naming the variable instead of surfacing later as a 500.

The auth API drops the admin bootstrap invite flow in favor of the OWNER_EMAIL grant the CLI already writes, so generated .env files no longer carry SEAMLESS_BOOTSTRAP_ENABLED, SEAMLESS_BOOTSTRAP_SECRET, or SEAMLESS_AUTH_DEBUG_SECRETS. AVAILABLE_ROLES now offers admin:read and admin:write, and Postgres TLS is configurable through DB_SSL, DB_SSL_CA, and DB_SSL_REJECT_UNAUTHORIZED.

Both API starters ship .env.example secret placeholders long enough to clear the adapter's 32 character minimum, so the documented cp .env.example .env && npm run dev path boots. A project from seamless init was already unaffected, because the CLI fills COOKIE_SIGNING_KEY itself.

Why the rest needed no CLI code

  • The API's standardized error shape consolidates onto { error }. loginFlow.ts and systemConfig.ts already read error first.
  • New env reaches generated projects on its own: the CLI fetches .env.example at the pinned tag and mutates the parsed result, so AVAILABLE_ROLES and the DB_SSL* keys flow through. v0.7.0 changed no env at all.
  • OWNER_EMAIL generation was already in place and was already supported by v0.5.0 (in src/lib/ownerAdmin.ts), just undocumented. Nothing was broken before this bump.
  • GET /system-config/public is consumed by the React SDK, not by the CLI. The CLI's own config command talks to the admin routes, which are unchanged.

Checks

npm run build clean, 691 unit tests pass, verify/harness tsc --noEmit clean.

Conformance passed on all three layers against auth API 0.7.1, @seamless-auth/express ^0.12.0, and @seamless-auth/react ^0.8.0, using clean checkouts at the pinned tags:

  ✔  API / adapter                 6.2s
  ✔  Web · react-vite (all flows)  11.5s
  ✔  Web · react-oauth (@oauth)    1.7s
  ✔ Conformance passed — 3 layer(s).

Two caveats on coverage, both worth knowing rather than blocking:

  • The harness has no Fastify layer. Its adapter app is Express only, so the Fastify starter is not exercised here. Tracked in feat: exercise the Fastify starter in the verify harness #147. The parity suite in seamless-auth-server asserts the two adapters return identical status, body, and Set-Cookie headers, including the console proxy, which is the current basis for confidence.
  • Verify builds the api and react layers from sibling working trees, not from the pinned refs. A checkout behind the tag will silently test the wrong source; the packages block in the summary reports the versions it actually used, which is the tell. The run above used clean v0.7.1 and v0.8.1 checkouts via SEAMLESS_API_DIR and SEAMLESS_TEMPLATES_DIR.

Bccorb added 2 commits July 30, 2026 12:29
Bump the pinned auth API to v0.6.0, the admin dashboard to v0.4.0, and the
templates ref to v0.6.0, which carries @seamless-auth/react ^0.7.0 in both
React starters and @seamless-auth/express ^0.11.0 in the Express starter.

The API drops the admin bootstrap invite flow in favor of the OWNER_EMAIL
grant the CLI already writes, adds admin:read and admin:write to
AVAILABLE_ROLES, and gains DB_SSL configuration. Those reach generated
projects without code changes: the CLI fetches .env.example at the pinned
tag and mutates it.

Move the conformance adapter to @seamless-auth/express ^0.11.0. Its
breaking change splits error into errorCode and errorBody on the handler
result types, which only affects code importing handlers from
@seamless-auth/core directly, so the adapter needed no source change.
Bump the templates ref to v0.7.0, which adds a Fastify API starter beside
Express. The backend prompt renders it as "Fastify (beta)" from the
registry status, so no CLI code changes are needed.

The starter serves the same surface as Express on the same env contract,
including the admin console at /console behind SERVE_ADMIN_CONSOLE, which
the Fastify adapter gained in @seamless-auth/fastify 0.2.0. Both API
starters now ship .env.example secrets long enough to clear the adapter's
32 character minimum.
@Bccorb Bccorb changed the title chore: move the scaffold onto the v0.6.0 ecosystem chore: move the scaffold onto the current ecosystem and offer Fastify Jul 30, 2026
Bccorb added 10 commits July 30, 2026 23:35
The auth API adds an unauthenticated GET /system-config/public returning the
configured login methods. The adapters proxy it, and the React starters read
it to offer a skip on the passkey registration screen when the instance has
another login method enabled. The API, adapters, and templates have to move
together for that path to work, so bump them as a set.

Templates v0.8.0 carries @seamless-auth/react ^0.8.0, @seamless-auth/express
^0.12.0, and @seamless-auth/fastify ^0.3.0, plus two Fastify boot fixes: an
empty PORT= now falls back to 3000 instead of binding a random free port, and
pino-pretty moves to a runtime dependency.

Move the conformance adapter to @seamless-auth/express ^0.12.0, the release
that proxies the new route.
Registering against a scaffolded Fastify API returned a 500 with
TypeError: option maxAge is invalid: 300. The auth server sent the
registration response's ttl as the string "300", and the Fastify adapter
handed it to a cookie library that requires an integer. Express never
showed this because its adapter multiplies the value into milliseconds,
coercing the string on the way past.

It is fixed from both ends. @seamless-auth/core 0.12.1 parses the lifetime
before it reaches an adapter and rejects anything that is not a positive
whole number of seconds, and auth API v0.7.1 sends a number. Templates
v0.8.1 carries the fix through the Fastify starter's committed lockfile,
which is what pinned the broken version.

The conformance adapter needs no pin change: it has no lockfile, so
^0.12.0 already resolves to the fixed 0.12.1.
Every command now answers -h / --help with usage, flags, subcommands, and
examples scoped to that command, and seamless help <command> prints the same
thing. The text lives in one registry (src/commands/helpTopics.ts) that both
the full seamless --help output and the per-command output render from, so a
flag is documented once. The dispatcher's known-command list comes from there
too.

The help check runs before a command parses its own arguments, so seamless
init -h prints help instead of scaffolding ./-h. A -- separator ends the check,
leaving room for a literal -h operand (seamless config set key -- -h).
Add `seamless templates list [--json]`, which prints every starter init can
scaffold with its id, kind, framework, selecting flags, and status. It reads the
same registry init does and needs no login, so the available templates no longer
have to be looked up in the source.

Every template now answers to `--<id>` as well as its shorter `--<alias>`, so
`init --react-vite` works alongside `--basic`, and the api starters get a flag
for the first time. The unknown-option error lists both spellings and points at
`templates list`.

Template flags are resolved in runCLI before a directory is created and before
the non-empty-directory confirmation, so an unrecognized or conflicting flag
fails immediately instead of surfacing only after the overwrite prompt. The
registry is opened at most once per run, and not at all on the integrate path.

Closes #149, closes #150, closes #151
`--yes` (-y) answers every question with the option the prompt marks as
recommended, so a scaffold runs from CI, a Dockerfile, or a script with no
terminal attached. Each question also gets its own flag, honored with or without
--yes: --web and --api choose the starters (by id or alias), --email sets the
owner who becomes the admin, --auth picks how the auth server runs, and --admin
picks where the console is hosted. Values are validated against the registry and
the known modes in runCLI, before a directory is created.

--yes stops rather than guessing in three places. Managed or local needs --app
or --local, because that decides where the project's auth lives for good.
Scaffolding into a non-empty directory needs --force, because starter files
overwrite anything with the same name. Rotating an application's existing
service token needs --force, because it breaks whatever is deployed on the old
one.

init's argument parsing moves into parseInitArgs so the new switches are not
mistaken for template flags or for the project name.

Closes #152
Run on a pipe, init used to render a @Clack prompt nobody could answer and wait
forever, so a CI step failed only when its job timed out. Every prompt in the
init flow is now fronted by requireInteractive, which stops on the first
unanswered question and names the flag that answers it. A run whose answers all
come from flags is unaffected and behaves the same on a pipe as on a terminal.

A terminal too narrow to render a prompt (a pty allocated without a size reports
one column, which printed one character per line) now warns rather than just
looking broken.

Scoped to init. The other commands that prompt are still unguarded, tracked in
the issue.

Closes #153
seamless init stopped rendering prompts to a pipe in #158; every other command
still hung there until its job timed out. Every prompt is now fronted by
requireInteractive, so a run without a TTY on stdin fails naming the flag that
answers the question: login, profile add, users delete,
users prepare-device-replacement, sessions revoke, org members remove,
config apply, and config oauth-providers remove.

A guard with nothing to point at would only convert a hang into a dead end, so
the destructive confirmations now take --force, matching what init means by it.
They share confirmDestructive, which answers itself when forced and otherwise
asks. hasForceFlag also accepts --yes and -y, because
config oauth-providers remove --yes shipped before the convention existed.
--force does not override --dry-run, and cancelling still reads as declining
rather than as an error.

The one-time code at login has no flag form (it does not exist until the code is
sent), so that step reports that it genuinely needs a terminal.

Tests get a setup file that marks stdin a TTY, since the guard would otherwise
fail every test that exercises a prompt; the no-terminal tests opt back out.

Closes #159
@Bccorb
Bccorb merged commit b4437a8 into main Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant