@rexezuge/identity 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The canonical `users` / `user_emails` schema.
3
+ *
4
+ * Converged from migration `0001_init.sql` + `0004_user_identity.sql` across the
5
+ * identity-carrying repos, expressed as the *final* shape so a fresh database
6
+ * never has to be upgraded: `users.id` is the account key from the start, and
7
+ * `user_emails` exists from the start.
8
+ *
9
+ * The invariants that make an address change safe (Durable-DAV's design, which
10
+ * is the strictest in the family and the one every other repo's upgrade is
11
+ * migrating toward):
12
+ *
13
+ * - `users.id` is an opaque, unique account key — never an address.
14
+ * - `users.email` is the **frozen anchor**: immutable, the foreign-key target
15
+ * every legacy `*_email` value resolves against, so history survives a change.
16
+ * - `users.current_email` is the mutable sign-in address; unique, so an address
17
+ * can never be claimed by two accounts.
18
+ * - `users.username` is a separately-mutable public handle, not the identity.
19
+ * - `user_emails` is the address registry: `is_verified = 1` may authenticate;
20
+ * a changed-from address stays at `0` (attributable, non-authenticating) and
21
+ * is re-pointable by a later claimant.
22
+ *
23
+ * The anchor design is forced on D1: it honours neither
24
+ * `PRAGMA foreign_keys = off` nor `legacy_alter_table`, and `defer_foreign_keys`
25
+ * does not suppress `ON DELETE CASCADE`, so a live reference cannot be repointed
26
+ * without losing rows. A fresh install just starts with the anchor in place.
27
+ *
28
+ * A repo with legacy tables that reference `users(email)` applies this file's
29
+ * statements and then adds its own additive backfill (the converged pattern is
30
+ * in `upgrade-user-identity.sql`), repointing each legacy column through
31
+ * `user_emails` rather than through the anchor.
32
+ */
33
+ const USERS_SCHEMA = `
34
+ -- The account. The anchor (email) is immutable; current_email is the login.
35
+ CREATE TABLE IF NOT EXISTS users (
36
+ id TEXT NOT NULL PRIMARY KEY,
37
+ email TEXT NOT NULL UNIQUE,
38
+ current_email TEXT NOT NULL UNIQUE,
39
+ username TEXT,
40
+ created_at INTEGER NOT NULL,
41
+ updated_at INTEGER
42
+ );
43
+
44
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_id ON users(id);
45
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_current_email ON users(current_email);
46
+
47
+ -- The address registry: which account an address authenticates.
48
+ CREATE TABLE IF NOT EXISTS user_emails (
49
+ email TEXT PRIMARY KEY,
50
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
51
+ is_verified INTEGER NOT NULL DEFAULT 0,
52
+ created_at INTEGER NOT NULL
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_user_emails_user ON user_emails(user_id);
56
+ `;
57
+ /**
58
+ * The additive upgrade for a database whose `users` predates the identity
59
+ * columns — converged from `0004_user_identity.sql` (Durable-DAV) with the
60
+ * table-specific phases (dav_volumes, namespaces) generalized into a
61
+ * documented extension point, because every repo's legacy tables differ.
62
+ *
63
+ * Run order per phase matches Durable-DAV's, and each backfill is guarded so
64
+ * the file is rerunnable: `users.id` is only filled where still missing, and
65
+ * every registry insert is `INSERT OR IGNORE`.
66
+ *
67
+ * Why the address stays in `users` at all (the anchor question, answered once):
68
+ * SQLite rewrites a child's foreign key clause when the parent is renamed and
69
+ * drops the parent by cascading, so the reference from a legacy
70
+ * `owner_email`-style column cannot be repointed at `users.id` without losing
71
+ * rows. Keeping `email` as a frozen anchor sidesteps the rebuild entirely:
72
+ * this migration is purely additive.
73
+ *
74
+ * A consumer with its own legacy tables appends one additive phase per table:
75
+ * add the `*_user_id` column with `REFERENCES users(id) ON DELETE CASCADE`
76
+ * (`NO ACTION` would *restrict* where the existing `owner_email` cascade
77
+ * removed rows — the two references must cascade together), backfill it by
78
+ * resolving the stored address through `user_emails`, and index it.
79
+ */
80
+ const UPGRADE_USER_IDENTITY = `
81
+ -- Phase 1: stable account key.
82
+ -- SQLite cannot add a PRIMARY KEY column, so the id is a plain column with a
83
+ -- unique index. A unique index is a valid foreign key parent, which is all the
84
+ -- user_id references need.
85
+ ALTER TABLE users ADD COLUMN id TEXT;
86
+
87
+ UPDATE users SET id = 'usr_' || lower(hex(randomblob(16))) WHERE id IS NULL;
88
+
89
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_id ON users(id);
90
+
91
+ -- Phase 2: mutable sign-in address.
92
+ -- 'email' stays as the frozen anchor; 'current_email' is what the account signs
93
+ -- in with and what the API reports. Uniqueness is enforced here, so an address
94
+ -- can never be claimed by two accounts.
95
+ ALTER TABLE users ADD COLUMN current_email TEXT;
96
+
97
+ UPDATE users SET current_email = lower(email) WHERE current_email IS NULL;
98
+
99
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_current_email ON users(current_email);
100
+
101
+ -- Phase 3: address registry.
102
+ -- Login resolution consults is_verified = 1 only. Backfilled from the frozen
103
+ -- anchor address of every existing account, lowercased so a legacy mixed-case
104
+ -- row still yields exactly one login identity.
105
+ CREATE TABLE IF NOT EXISTS user_emails (
106
+ email TEXT PRIMARY KEY,
107
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
108
+ is_verified INTEGER NOT NULL DEFAULT 0,
109
+ created_at INTEGER NOT NULL
110
+ );
111
+
112
+ CREATE INDEX IF NOT EXISTS idx_user_emails_user ON user_emails(user_id);
113
+
114
+ INSERT OR IGNORE INTO user_emails (email, user_id, is_verified, created_at)
115
+ SELECT lower(email), id, 1, created_at FROM users;
116
+ `;
117
+ /**
118
+ * Every migration this package ships, in apply order.
119
+ *
120
+ * `upgrade-user-identity.sql` is *not* applied on a fresh database — the
121
+ * fresh-install path is `users.sql` alone, and the upgrade exists only for a
122
+ * legacy one.
123
+ */
124
+ const IDENTITY_MIGRATIONS = [
125
+ { name: '0001_users.sql', sql: USERS_SCHEMA },
126
+ { name: '0002_upgrade_user_identity.sql', sql: UPGRADE_USER_IDENTITY },
127
+ ];
128
+ export { IDENTITY_MIGRATIONS, UPGRADE_USER_IDENTITY, USERS_SCHEMA };
129
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoC7B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,mBAAmB,GAAiD;IACxE,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,YAAY,EAAE;IAC7C,EAAE,IAAI,EAAE,gCAAgC,EAAE,GAAG,EAAE,qBAAqB,EAAE;CACvE,CAAC;AAEF,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,YAAY,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@rexezuge/identity",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Rexezuge-CloudflareWorkers/SDK.git",
9
+ "directory": "packages/identity"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./*": {
20
+ "types": "./dist/*.d.ts",
21
+ "default": "./dist/*.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "dependencies": {
28
+ "@rexezuge/d1": "1.0.0",
29
+ "@rexezuge/errors": "1.0.0",
30
+ "@rexezuge/shared": "1.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "26.6.4",
34
+ "typescript": "6.0.3",
35
+ "vitest": "4.1.11"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
40
+ "test": "vitest run"
41
+ }
6
42
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.