A sleep that returned in 181ms and a 50ms TTL with 20ms of slack were
reporting how busy the box was rather than whether the code works.
Both keep the assertion that matters -- sleep does not return early, a
re-Put outlives the first expiry -- with margins jitter cannot reach:
the TTL test still fails three times in three when Put stops
refreshing expiresAt.
Co-authored-by: Hanzo Dev <[email protected]>
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.
Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
An extension's hooks were stated once, on the Base the process started with,
so a write to a tenant's Base fired nothing an extension had asked for. The
file says what to do when a record is created; it does not say where, and the
where comes with the event.
So a declaration is recorded as something that can be stated on any Base, and
stated on each as it is constructed — the same place realtime is bound, for
the same reason. A hook still sees only the Base it fired on.
This grants an extension nothing it did not have. A file on disk is the
operator's, and it already ran with the process's authority; what changes is
which data its hooks see. An extension written when there was one Base now
runs against each, which is a behaviour change and is the thing to know.
Routes and crons an extension declares stay on the Base that loaded it: a
route is registered once on a router, and a schedule belongs to the process.
A tenant's Base was bootstrapped and never served, so nothing it scheduled
ever ran — its logs were never cleaned and its database never optimised.
Co-authored-by: Hanzo Dev <[email protected]>
The verification and email-change templates were the last of local auth. Their
endpoints went in the IAM-native rip — bindRecordAuthApi keeps auth-methods and
auth-refresh and nothing else — so both answered 404 for every deployment, not
only IAM ones, and their buttons addressed a route in an admin SPA that no
longer exists. Re-pathing them last commit made them honest; this removes them,
along with everything that existed only to serve them.
That turned out to be a lot, and each piece was already dead on its own terms:
core.EmailTemplate and the two collection fields — no other user
the mails package entire — its two functions were the only callers of its
own template resolver and its own html layout
the $mails JSVM namespace — an empty object once the two binds went, and its
type declarations advertised FIVE helpers, three of which (password reset,
OTP, auth alert) were removed in that same rip and never bound at all, so
hook authors have been reading a contract for functions that do not exist
OnMailerRecordVerificationSend / OnMailerRecordEmailChangeSend — nothing has
been able to fire either since the mails package was their only caller
core.MailerRecordEvent — the type those two hooks carried
Nothing loses a capability. $app.newMailClient().send() is the general sender,
and it is what an app binding its own confirm route would have used anyway; the
two removed binds were convenience wrappers over it.
POST /v1/settings/test/email stays and gets better. It rendered a template
against a throwaway record, so "can this deployment deliver mail" was answerable
only through a flow that no longer exists. It sends a plain message now — no
template, no collection, no record, no token — which is the question an operator
has after editing SMTP settings.
The collection fields are DEFAULTS applied at creation; a collection stores its
own copy, and an options key the struct no longer declares is ignored on read,
so nothing already deployed changes shape. types.d.ts is regenerated by its own
generator rather than hand-edited (the generator still listed the deleted
package, which is why it had to be fixed first).
Base's own registry was already org-native in every method — OrgDB(orgID),
HasOrg(orgID), orgDir(orgID) — and only the type names said tenant. So this is
the vocabulary catching up with the code, not a change of behaviour.
TenantRegistry -> Bases app.Bases().OrgDB(org)
TenantConfig -> BasesConfig
MultiTenant -> PerOrg it enables per-org databases; say that
MULTI_TENANT -> MULTI_BASE
core/tenant.go -> core/bases.go
Tenant was the wrong noun twice over: it is the multi-tenancy word, and this
product's unit is the thing a customer creates per org — a Base. Identity here
is org, user, project, and nothing else.
The UI never said it; this was Go-side only. 64 packages green.
One test was collateral and is fixed rather than renamed: TestToHTTPHeader
asserts a pure snake_case-to-Header-Case transform, so rewriting its EXPECTED
value while its input stayed "tenant_id" made it assert something false. The case
is dropped — org_id already covers that transform.
`plugins/jsvm/internal/types/generated/types.d.ts` carried literal
`<<<<<<< HEAD` / `=======` / `>>>>>>> upstream/master` at lines 23807-23850,
committed, from an old upstream merge nobody finished. The file's own first
line reads GENERATED CODE - DO NOT MODIFY BY HAND, so it was not hand-edited
around: `make jstypes` (go run ./plugins/jsvm/internal/types/types.go) was run,
which is the one way this file is produced.
Regenerating showed the markers were the smaller half of the problem — the file
was badly stale. Nothing was lost (every namespace present before is still
present, `declare function` count unchanged at 17) and NINE namespaces appear
that Base's own packages have exported for some time: crdt, tasks, network,
metric, logger, query, json, big, rsa. Hook authors writing JS against Base had
no types for any of them.
Worth knowing for whoever adds a regenerate-and-diff gate: the generator
prepends `// <unix timestamp>` to line 1, so this file is never byte-reproducible
across runs and such a gate must ignore that line.
Co-authored-by: Hanzo Dev <[email protected]>
Rename / namespace:
- Finish github.com/pocketbase/* → github.com/hanzoai/base (+ /dbx) sweep
across all Go sources; restore the renamed root launcher (Base, base.go).
- Drop the upstream-only surfaces ours doesn't ship (admin ui/, the
password/OTP/oauth2/impersonate auth files, extensions) re-added by the
upstream merge.
- Decomplect the on-disk layout to one base_/ namespace: data/, migrations/,
hooks/, types.d.ts are siblings under base_/ (default data dir
base_/data). Drop the hz_ prefix everywhere; collapse migratecmd's
identical JS/Go branches to one path.
- Remove the accidentally-committed hz_data/ dev dir (db + iam.key) and
gitignore the runtime namespace dirs.
Deps / build:
- kmsclient now imported from the light github.com/hanzoai/kms/sdk/go module
(the root kms module zip exceeds Go's download limit); cloud/pkg/base
Mount removed (lives in the fused daemon).
- replicate v0.4.0 → v0.8.0; metric.Counter.Get() (no prometheus dto);
network/metrics.go import fix; auth gitlab/github tidy.
Tests / behavior:
- Wire registerNotifyWatcherHooks; debounce notify files past the kqueue
rescan window so cross-instance settings/collections reload fires.
- migratecmd: exclude createdAt/updatedAt (not created/updated); regenerate
Go+JS automigration goldens from canonical output.
- Convert the two stale archive red-probes to assert the landed defenses
(backlog cap bounds memory; forged-segment signature is rejected).
IAM-native (Base = pure IAM client):
- Rip the ~1k-line home-grown embedded IAM (iam_embedded*.go) and the
iam-user CLI. Base no longer reinvents identity as _iam_users/_superusers
rows. IAM_ENDPOINT (hanzo.id, or an in-process iam.Embed in the fused
daemon) is required; Base validates IAM JWTs via JWKS only — superuser is
the IAM admin claim, resolved to an ephemeral unpersisted record.
- Extract the IDV reverse-proxy (/v1/idv/*) into idv_proxy.go — it was
mis-colocated in the embedded onboarding file and is independent of IAM.
- Bind the SQL console (/v1/sql) and test it through real IAM JWTs against a
JWKS stub (no hardcoded record tokens, no persisted rows).
Every non-handler call site that wrapped a value in
slog.String/Int/Bool/Any/Float64/Uint64 was passing an Attr through the
variadic any... slot of (*slog.Logger).Info/Warn/Error/Debug; the same
slot accepts the bare "key", value pair with identical semantics. Drop
the wrapper so the surface is one level less indirect and so the next
two commits can swap (*slog.Logger) for a small Logger interface
without rewriting every attr expression as well.
23 files, 156 call sites collapsed; no behavior change. Five files —
tools/logger/log.go, tools/logger/batch_handler.go +_test, and
core/log_printer.go +_test — are the handler-implementation layer and
legitimately reference slog.Attr/Level/Group; they were skipped.
Sweep the surviving auth-side tests so they no longer assert on
deleted constants / models / config knobs:
- collection / record exports + indexes / record-table-sync drop
the password column from auth-collection bootstrap fixtures and
the JSON snapshots
- TestRecordPublicExportAndMarshalJSON keeps the legacy "drop
password key from auth-record export" invariant (now enforced as
a hard-coded delete on the record_model.go side, not via the
field type) so accidental Set("password", x) calls never leak
- api-layer crud/auth-methods/collection-import scenarios drop the
"password / passwordConfirm / oldPassword / MFA / OTP" expected
keys and switch the auth-collection-create error fixtures to
email-validation + manager-access scenarios that still exist
- migrate-cmd / jsvm bind counts updated; jsvm $mails surface
drops to {verification, email-change}; PasswordField + the
sendRecordPasswordReset / sendRecordOTP / sendRecordAuthAlert
bindings removed from the type generator's expected fixture
- migrations/1747094400 raw-SQL implementation: the old delete
path went through the collection model (which can't rehydrate
pre-rip rows that still encode the deleted password field type)
and the system-collection guard. New approach: DROP TABLE IF
EXISTS + DELETE FROM _collections WHERE name=$x, then strip the
password entry from each surviving auth collection's JSON
fields array and ALTER TABLE DROP COLUMN. Idempotent on fresh
installs (no legacy table → no-op).
apis ./core ./plugins/jsvm ./forms all run green now.
- forms/RecordUpsert no longer manages a plain password / confirm /
oldPassword triple; Load + validateFormFields only handle the
email + verified system fields now. DrySubmit + Submit semantics
unchanged
- apis/installer + apis/superuser_auth_oidc drop SetRandomPassword;
the installer superuser and the OIDC auto-create path persist a
verified passwordless record (IAM handles credentials)
- apis/collection_test snapshots updated for the slimmer auth
collection bootstrap (no system "password" field) and the smaller
collection inventory (12 not 16); the now-stale "create-base-
collection-with-oldPassword-rejected" test is deleted because the
reserved-key check is gone
- plugins/jsvm drops the PasswordField bind
- plugins/zap returns 410 Gone for /identity (the only ZAP path
that called record.ValidatePassword) — IAM PKCE is the only login
- plugins/migratecmd test no longer probes a PasswordResetToken
secret on auth collections
- tests/dynamic_stubs loses StubMFARecords + StubOTPRecords;
tests/token_helper loses GetUserPasswordResetToken
App.OnRecordAuthWithPasswordRequest, OnRecord{Request,Confirm}-
PasswordResetRequest, OnRecord{Request,Authenticate}OTPRequest,
OnMailerRecordPasswordResetSend, OnMailerRecordOTPSend and
OnMailerRecordAuthAlertSend were vestigial after the 16-handler purge
in eda3ae3a — every callable that wired them up is gone. Drop the
interface methods, the event types, the BaseApp fields, the init/getter
plumbing, and the matching mails.SendRecordPasswordReset /
SendRecordOTP / SendRecordAuthAlert senders. The superuser
PasswordReset filter goes with them; IAM owns superuser password flow.
The superuser collection save handler no longer forces PasswordAuth
on or pairs OTP with MFA — _superusers is now a pure email-only
mirror; IAM PKCE is the only way in.
Replace github.com/pocketbase/pocketbase with github.com/hanzoai/base
in 106 Go source files. Keep pocketbase/dbx and pocketbase/tygoja as-is
(upstream dependencies). Add compiled binary to .gitignore. Include
realtime DX design document inspired by Convex patterns.