`db:migrate` on the catalog API stopped before running anything:
Multiple migrations have the version number 20260819000001.
The engine appends its `db/migrate` to the host's migration paths instead of
copying migrations in, so engine and host share one version namespace. Both had
picked 20260819000001 on the same day by the same habit — the engine for
device_grants, apps/api for carry_the_store_config_in_the_registry — and neither
repository could see the other's number.
Worse than a clash of our own making: it takes the *host's* migrations down with
it, for the whole application, before anything runs.
device_grants is renumbered to 20260819093412 — a real second-resolution
timestamp, which is the actual defence. A round hand-written number is precisely
what another repository lands on. Nothing had run it in production, so this is a
rename rather than a data migration; a host that already applied the old version
renumbers its schema_migrations row.
The older engine migrations keep their round numbers: renumbering one that has
been deployed everywhere is worse than the risk it carries. They are named in the
new spec's grandfather list rather than excused by a rule that would also let the
next one through.
That spec found a second thing, older than this change: the install template
creates `application_tokens`, and so does one of our own migrations — so a fresh
host runs CREATE TABLE twice and has to delete the block from its generated copy
by hand, which is exactly what teletype-orbit's migration header describes. I had
just made it worse by putting device_grants in the template too; that is out
again, and the template says why. `application_tokens` is grandfathered and left
for a change that is not a hotfix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
36 lines
2.0 KiB
Ruby
36 lines
2.0 KiB
Ruby
class CreateDeviceGrants < ActiveRecord::Migration[8.1]
|
|
def change
|
|
create_table :device_grants, id: { type: :bigint, unsigned: true },
|
|
charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci" do |t|
|
|
# Both codes are secrets in the sense that guessing one grants a session, but they
|
|
# have different jobs: the device code is long and never shown to a person, the
|
|
# user code is short enough to read off a screen and type into a browser.
|
|
t.string :device_code, limit: 64, null: false
|
|
t.string :user_code, limit: 16, null: false
|
|
# What the client called itself. Shown on the approval page, so a person can tell
|
|
# which machine is asking, and kept on the issued token as its name.
|
|
t.string :client_name, limit: 128
|
|
# The approving subject comes from the host
|
|
# (WarpEngine.config.access_token_owner_class), so no FK — same reasoning as
|
|
# application_tokens.owner_type.
|
|
t.string :subject_type, limit: 128
|
|
t.bigint :subject_id, unsigned: true
|
|
t.bigint :application_token_id, unsigned: true
|
|
# The issued token, in the clear, for the seconds between "approved" and "the
|
|
# client's next poll". It is cleared on the poll that hands it over, so this
|
|
# column holds a live secret only while somebody is waiting for it. There is no
|
|
# way around storing it: the approval happens in a browser and the poll arrives on
|
|
# a different request, so the two cannot share memory. Everything else about a
|
|
# token is stored as a digest — this is the one exception, and it is temporary.
|
|
t.string :issued_token, limit: 64
|
|
t.datetime :approved_at, precision: 3
|
|
t.datetime :denied_at, precision: 3
|
|
t.datetime :expires_at, precision: 3, null: false
|
|
t.timestamps precision: 3, null: true
|
|
t.index :device_code, name: "idx_device_grants_device_code", unique: true
|
|
t.index :user_code, name: "idx_device_grants_user_code", unique: true
|
|
t.index :expires_at, name: "idx_device_grants_expires_at"
|
|
end
|
|
end
|
|
end
|