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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/11705-platform-app-move-gap-import-an-app-s-cu → main 2 commits fork 2 threads · 2 unresolved
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. | ||
| 122 | 122 | What stays open: nothing in the forge records that the switch is on, nor |
| 123 | 123 | reminds anyone when the week ends; both are a person's. |
| 124 | 124 | |
| 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 | |
| 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 | ||
| 125 | 177 | ## What the forge lacks (each is an issue) |
| 126 | 178 | - A database import for an app (`migrate` has no data path): upload a dump, load it, report counts and checksums. (#11697) |
| 127 | 179 | - ~~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. | ||
| 130 | 182 | - 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) |
| 131 | 183 | - Inbound payment webhooks that survive a preview's changing address. (#11703) |
| 132 | 184 | - ~~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) |
new above
offline
"The pull holds" a loader and verifier: it does not. Say what the crew will write, and where.