Public: anyone can read this; the crew can change it.

Platform app move gap: Import an app's customer accounts without reading the secrets #11731

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/11705-platform-app-move-gap-import-an-app-s-cu → main 2 commits fork 2 threads · 2 unresolved

  • Merged. mason/11705-platform-app-move-gap-import-an-app-s-cu was merged into main as 688450f.

Review

Sign in to review this change.
1 file changed +53 −1
docs/features/platform-app-move.md +53 −1
@@ -122,6 +122,58 @@ reachable from a crew run); try them on a scratch copy first.
122122 What stays open: nothing in the forge records that the switch is on, nor
123123 reminds anyone when the week ends; both are a person's.
124124
125 ## 8. Customer accounts, without the crew reading them (person runs, crew writes)
126 Accounts are step 3's data with one more rule: the crew never sees a row of
127 them, not an email, not a hash. **Nothing here was run**; the forge has no
128 feature for it (the gap below), so it is a procedure and a script.
129 1. **Formats are kept, not converted.** A password hash cannot be turned into
130 another format without the password. Carry each hash exactly as stored
131 (`$2b$` bcrypt, `$argon2id$` argon2, `scrypt$`/`pbkdf2$` with their salt
132 and cost parameters in the string). The inventory (step 1) names which
133 formats the old app wrote, by prefix only, and whether a **pepper** (a
134 server-side secret mixed in before hashing) was used. A pepper is a secret
135 the owner sets by name in the new app's settings (step 3); without it
136 every pepper-hashed account fails, so say so before cut-over.
137 2. **The crew writes against invented data.** In the migrated app's own
138 repository (not in the forge, and **not yet written**), the crew will write
outdated

"The pull holds" a loader and verifier: it does not. Say what the crew will write, and where.

139 (a) a loader that reads the owner's export file and writes the new `bun:sqlite` table
140 without logging a row, and (b) a verifier that recognises each format by
141 its prefix, with tests on made-up accounts and hashes made in the test.
142 An unknown prefix is refused, never guessed at. Add the rehash: after a
143 successful sign-in with an old format, store the new app's own format.
144 3. **The owner exports and loads.** On the old node, export only the columns
145 the new app needs (id, email, hash, created-at, and flags such as
146 verified or staff), to a file that stays on the owner's machine. Run the
147 loader on the owner's own machine against a copy of the new app's
outdated

"or on the preview's data directory" contradicts "stays on the owner's machine"; no door exists for the file to reach a preview unseen (#11697). Remove or explain.

148 database, then hand the loaded database over by the data copy of step 3.
149 The file never goes to a preview or a run: the forge has no way to receive
150 it unseen (#11697), so until that exists the owner, not the crew, does the
151 load. The export file is deleted after the load. Do not export sessions, reset tokens or API keys: sign
152 everyone out and let reset mail do its work.
153 4. **Report counts, never content.** The loader prints per-format counts
154 (`bcrypt 1204, argon2id 31`), rows loaded, rows refused with the reason,
155 and an HMAC-SHA-256 over the ordered `id` and `hash` pairs, computed on
156 both sides with a throwaway key the owner makes and keeps. It must be
157 keyed: an unkeyed digest lets anyone holding candidate hashes confirm
158 them. Equal numbers and digests go in the issue; the key does not. Duplicate emails differing by case are listed by count and
159 resolved by the owner.
160 5. **Prove it with the owner's own account.** In the rehearsal (step 4) the
161 owner, or a test customer they create on the old app first, signs in on
162 the preview. One working sign-in per format is the check; the crew is
163 told "works" or "fails", never the password or the row.
164 6. **Cut-over order.** Freeze the old side (section 7), export again so
165 accounts made since the rehearsal are in, load, compare, then switch the
166 domain. Accounts changed on the old side after the last export are lost;
167 the freeze is what prevents that.
168 7. **After.** Delete the export file and any copy; the step-5 backup is the
169 only other copy and is encrypted. Remove the pepper from the old node only
170 when the old app is switched off.
171
172 What stays open: nothing stops an export file being pasted somewhere it
173 should not be; the rule is a person's. OAuth accounts (Google and so on)
174 carry a provider id, not a hash, and the new app needs the same client
175 credentials set by name or those people sign in as new accounts.
176
125177 ## What the forge lacks (each is an issue)
126178 - A database import for an app (`migrate` has no data path): upload a dump, load it, report counts and checksums. (#11697)
127179 - ~~A Postgres app has no home here.~~ Decided, not built: no managed Postgres; port to `bun:sqlite` or keep an external one by `egress` (docs/APPS.md). (#11698)
@@ -130,4 +182,4 @@ reminds anyone when the week ends; both are a person's.
130182 - A guard for rehearsal: data copy and `preview`-scoped sandbox keys exist and are the procedure in step 4; nothing yet refuses a payment name left in scope `all`. (#11702)
131183 - Inbound payment webhooks that survive a preview's changing address. (#11703)
132184 - ~~A read-only switch for the old side of a migration.~~ A procedure at the old proxy and database, section 7; the forge cannot reach the old node, so no feature is built. (#11704)
133 - Importing an app's customer accounts (password-hash formats) without the owner reading them. (#11705)
185 - Importing an app's customer accounts without the crew reading them. Section 8 is the procedure; the loader and verifier it names are still to be written in the migrated app, and the file's way into a preview is #11697. (#11705)

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs