@govcore/schema 0.2.0 → 0.2.1

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.
package/dist/index.js CHANGED
@@ -1,26 +1,231 @@
1
- // @govcore/schema — platform table definitions + schema version.
2
- // Edge-safe entrypoint (no DB client). The migrate runner is ./migrate.
3
- export * from './schema';
4
- /**
5
- * True when `err` is (or wraps) a Postgres unique-constraint violation
6
- * (SQLSTATE 23505). Pure and edge-safe — inspects `code`, touches no DB client.
7
- * Lets operator flows turn a duplicate slug/email into a typed result instead of
8
- * a 500. Walks the `cause` chain because Drizzle wraps the driver error in a
9
- * `DrizzleQueryError` and puts the `postgres-js`/`pg` error (which carries the
10
- * `code`) on `.cause` — so the top-level error's own `code` is undefined.
11
- */
12
- export function isUniqueViolation(err) {
13
- for (let e = err, depth = 0; e && typeof e === 'object' && depth < 5; depth++) {
14
- if (e.code === '23505')
15
- return true;
16
- e = e.cause;
17
- }
18
- return false;
1
+ // src/schema.ts
2
+ import {
3
+ boolean,
4
+ integer,
5
+ jsonb,
6
+ pgSchema,
7
+ text,
8
+ timestamp,
9
+ unique,
10
+ uniqueIndex,
11
+ uuid
12
+ } from "drizzle-orm/pg-core";
13
+ var govcore = pgSchema("govcore");
14
+ var visibility = govcore.enum("visibility", ["org", "connections", "instance"]);
15
+ var federationStatus = govcore.enum("federation_status", ["pending", "active", "rejected"]);
16
+ var organizations = govcore.table(
17
+ "organizations",
18
+ {
19
+ id: uuid("id").primaryKey().defaultRandom(),
20
+ name: text("name").notNull(),
21
+ slug: text("slug").notNull(),
22
+ /** App-extensible bag for org settings GovCore itself doesn't model. */
23
+ metadata: jsonb("metadata").notNull().default({}),
24
+ createdAt: timestamp("created_at").notNull().defaultNow(),
25
+ updatedAt: timestamp("updated_at").notNull().defaultNow()
26
+ },
27
+ (t) => [uniqueIndex("organizations_slug_unique").on(t.slug)]
28
+ );
29
+ var orgScoped = (orgs) => ({
30
+ organizationId: uuid("organization_id").notNull().references(() => orgs.id, { onDelete: "cascade" })
31
+ });
32
+ var users = govcore.table(
33
+ "users",
34
+ {
35
+ id: uuid("id").primaryKey().defaultRandom(),
36
+ organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
37
+ /** Last-selected active org, honored first by active-membership resolution. */
38
+ lastActiveOrganizationId: uuid("last_active_organization_id").references(
39
+ () => organizations.id,
40
+ { onDelete: "set null" }
41
+ ),
42
+ name: text("name"),
43
+ email: text("email").notNull(),
44
+ emailVerified: timestamp("email_verified"),
45
+ image: text("image"),
46
+ passwordHash: text("password_hash"),
47
+ /**
48
+ * App-defined role (see @govcore/rbac `createRbac`) — stored as `text`, not
49
+ * a fixed enum, so GovCore ships no role vocabulary of its own. Denormalized
50
+ * cache of the active membership's role; nullable so the Auth.js adapter can
51
+ * create a user before a membership is assigned.
52
+ */
53
+ role: text("role"),
54
+ /** Platform-level role (e.g. instance/platform admin), separate from per-org role. */
55
+ instanceRole: text("instance_role"),
56
+ isActive: boolean("is_active").notNull().default(true),
57
+ // Account-lockout + password-expiry state; the policy itself lives app-side.
58
+ failedLoginAttempts: integer("failed_login_attempts").notNull().default(0),
59
+ lockoutUntil: timestamp("lockout_until"),
60
+ lastPasswordChangedAt: timestamp("last_password_changed_at").notNull().defaultNow(),
61
+ createdAt: timestamp("created_at").notNull().defaultNow(),
62
+ updatedAt: timestamp("updated_at").notNull().defaultNow()
63
+ },
64
+ // One identity per email across all orgs — unambiguous auth/SSO lookups.
65
+ (t) => [uniqueIndex("users_email_unique").on(t.email)]
66
+ );
67
+ var accounts = govcore.table("accounts", {
68
+ id: uuid("id").primaryKey().defaultRandom(),
69
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
70
+ type: text("type").notNull(),
71
+ provider: text("provider").notNull(),
72
+ providerAccountId: text("provider_account_id").notNull(),
73
+ refreshToken: text("refresh_token"),
74
+ accessToken: text("access_token"),
75
+ expiresAt: timestamp("expires_at"),
76
+ tokenType: text("token_type"),
77
+ scope: text("scope"),
78
+ idToken: text("id_token"),
79
+ sessionState: text("session_state")
80
+ });
81
+ var sessions = govcore.table("sessions", {
82
+ id: uuid("id").primaryKey().defaultRandom(),
83
+ sessionToken: text("session_token").notNull().unique(),
84
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
85
+ expires: timestamp("expires").notNull()
86
+ });
87
+ var verificationTokens = govcore.table("verification_tokens", {
88
+ identifier: text("identifier").notNull(),
89
+ token: text("token").notNull(),
90
+ expires: timestamp("expires").notNull()
91
+ });
92
+ var userOrganizationMemberships = govcore.table(
93
+ "user_organization_memberships",
94
+ {
95
+ id: uuid("id").primaryKey().defaultRandom(),
96
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
97
+ organizationId: uuid("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
98
+ /** App-defined role for this membership (text, not an enum — see `users.role`). */
99
+ role: text("role").notNull(),
100
+ /** Revocation soft-deactivates so the historical row survives for audit. */
101
+ isActive: boolean("is_active").notNull().default(true),
102
+ isPrimary: boolean("is_primary").notNull().default(false),
103
+ createdAt: timestamp("created_at").notNull().defaultNow(),
104
+ updatedAt: timestamp("updated_at").notNull().defaultNow()
105
+ },
106
+ (t) => [uniqueIndex("user_org_membership_unique").on(t.userId, t.organizationId)]
107
+ );
108
+ var auditLog = govcore.table("audit_log", {
109
+ id: uuid("id").primaryKey().defaultRandom(),
110
+ organizationId: uuid("organization_id"),
111
+ // historical UUID — no FK on purpose
112
+ userId: uuid("user_id"),
113
+ // historical UUID — no FK on purpose
114
+ action: text("action").notNull(),
115
+ entityType: text("entity_type").notNull(),
116
+ entityId: uuid("entity_id"),
117
+ before: jsonb("before"),
118
+ after: jsonb("after"),
119
+ metadata: jsonb("metadata"),
120
+ createdAt: timestamp("created_at").notNull().defaultNow()
121
+ });
122
+ var orgConnections = govcore.table(
123
+ "org_connections",
124
+ {
125
+ id: uuid("id").primaryKey().defaultRandom(),
126
+ fromOrgId: uuid("from_org_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
127
+ toOrgId: uuid("to_org_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
128
+ status: federationStatus("status").notNull().default("pending"),
129
+ createdBy: uuid("created_by").references(() => users.id),
130
+ createdAt: timestamp("created_at").notNull().defaultNow(),
131
+ updatedAt: timestamp("updated_at").notNull().defaultNow()
132
+ },
133
+ (t) => [unique("unique_org_connection").on(t.fromOrgId, t.toOrgId)]
134
+ );
135
+ var crossOrgLinks = govcore.table("cross_org_links", {
136
+ id: uuid("id").primaryKey().defaultRandom(),
137
+ sourceOrgId: uuid("source_org_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
138
+ sourceEntityType: text("source_entity_type").notNull(),
139
+ sourceEntityId: uuid("source_entity_id").notNull(),
140
+ targetOrgId: uuid("target_org_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
141
+ targetEntityType: text("target_entity_type").notNull(),
142
+ targetEntityId: uuid("target_entity_id").notNull(),
143
+ linkType: text("link_type").notNull(),
144
+ status: federationStatus("status").notNull().default("pending"),
145
+ rejectionReason: text("rejection_reason"),
146
+ flaggedForReview: boolean("flagged_for_review").notNull().default(false),
147
+ flagReason: text("flag_reason"),
148
+ createdBy: uuid("created_by").references(() => users.id),
149
+ createdAt: timestamp("created_at").notNull().defaultNow(),
150
+ updatedAt: timestamp("updated_at").notNull().defaultNow()
151
+ });
152
+ var breakGlassSessions = govcore.table("break_glass_sessions", {
153
+ id: uuid("id").primaryKey().defaultRandom(),
154
+ instanceAdminId: uuid("instance_admin_id").notNull().references(() => users.id),
155
+ targetOrgId: uuid("target_org_id").notNull().references(() => organizations.id),
156
+ reason: text("reason").notNull(),
157
+ grantedAt: timestamp("granted_at").notNull().defaultNow(),
158
+ expiresAt: timestamp("expires_at").notNull(),
159
+ requiresApproval: boolean("requires_approval").notNull().default(false),
160
+ approvedAt: timestamp("approved_at"),
161
+ approvedBy: uuid("approved_by").references(() => users.id),
162
+ revokedAt: timestamp("revoked_at"),
163
+ revokedBy: uuid("revoked_by").references(() => users.id)
164
+ });
165
+ var actAsSessions = govcore.table("act_as_sessions", {
166
+ id: uuid("id").primaryKey().defaultRandom(),
167
+ breakGlassSessionId: uuid("break_glass_session_id").notNull().references(() => breakGlassSessions.id),
168
+ instanceAdminId: uuid("instance_admin_id").notNull().references(() => users.id),
169
+ targetOrgId: uuid("target_org_id").notNull().references(() => organizations.id),
170
+ startedAt: timestamp("started_at").notNull().defaultNow(),
171
+ expiresAt: timestamp("expires_at").notNull(),
172
+ endedAt: timestamp("ended_at"),
173
+ endReason: text("end_reason")
174
+ });
175
+ var ACT_AS_DEFAULT_TTL_MINUTES = 30;
176
+ var ACT_AS_END_REASONS = [
177
+ "admin_ended",
178
+ "expired",
179
+ "parent_revoked",
180
+ "parent_expired"
181
+ ];
182
+ var instanceSettings = govcore.table("instance_settings", {
183
+ id: uuid("id").primaryKey().defaultRandom(),
184
+ disabledModules: jsonb("disabled_modules").$type().notNull().default({}),
185
+ createdAt: timestamp("created_at").notNull().defaultNow(),
186
+ updatedAt: timestamp("updated_at").notNull().defaultNow()
187
+ });
188
+ var platformConfig = govcore.table("platform_config", {
189
+ id: text("id").primaryKey().default("singleton"),
190
+ instanceName: text("instance_name").notNull().default("GovCore"),
191
+ defaultTheme: text("default_theme").notNull().default("base"),
192
+ allowLocalAuth: boolean("allow_local_auth").notNull().default(true),
193
+ /** Stamped onto new orgs at provisioning time. Null means no tier. */
194
+ defaultSupportTier: text("default_support_tier"),
195
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
196
+ updatedBy: uuid("updated_by").references(() => users.id, { onDelete: "set null" })
197
+ });
198
+
199
+ // src/index.ts
200
+ function isUniqueViolation(err) {
201
+ for (let e = err, depth = 0; e && typeof e === "object" && depth < 5; depth++) {
202
+ if (e.code === "23505") return true;
203
+ e = e.cause;
204
+ }
205
+ return false;
19
206
  }
20
- /**
21
- * Bumped whenever the platform schema changes. Written to instance settings on
22
- * boot for observability ("this instance runs platform schema vN", design §5),
23
- * and stamped into backup archives (design §13.5). Not load-bearing for
24
- * correctness — the migrations are — but useful for diagnostics.
25
- */
26
- export const CORE_SCHEMA_VERSION = '0.0.0';
207
+ var CORE_SCHEMA_VERSION = "0.0.0";
208
+ export {
209
+ ACT_AS_DEFAULT_TTL_MINUTES,
210
+ ACT_AS_END_REASONS,
211
+ CORE_SCHEMA_VERSION,
212
+ accounts,
213
+ actAsSessions,
214
+ auditLog,
215
+ breakGlassSessions,
216
+ crossOrgLinks,
217
+ federationStatus,
218
+ govcore,
219
+ instanceSettings,
220
+ isUniqueViolation,
221
+ orgConnections,
222
+ orgScoped,
223
+ organizations,
224
+ platformConfig,
225
+ sessions,
226
+ userOrganizationMemberships,
227
+ users,
228
+ verificationTokens,
229
+ visibility
230
+ };
231
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/index.ts"],"sourcesContent":["// @govcore/schema — platform table definitions (identity, tenancy, auth, audit).\n//\n// Edge-safe: imports only `drizzle-orm/pg-core`, no DB client. The app imports\n// these for type-safe queries and to declare foreign keys *to* platform tables.\n// The govcore-migrate runner (./migrate) is a separate, non-edge entrypoint.\n//\n// All platform tables live in a dedicated `govcore` Postgres schema (design\n// §13.4) so they never collide with an app's `public` tables and ownership is\n// obvious. The authored DDL lives in ../migrations and must stay in sync.\n\nimport {\n type AnyPgColumn,\n boolean,\n integer,\n jsonb,\n pgSchema,\n text,\n timestamp,\n unique,\n uniqueIndex,\n uuid,\n} from 'drizzle-orm/pg-core'\n\n/** The Postgres schema every GovCore platform table belongs to. */\nexport const govcore = pgSchema('govcore')\n\n/** Content/federation visibility. Used by the federation package (later phase). */\nexport const visibility = govcore.enum('visibility', ['org', 'connections', 'instance'])\n\n/** Generic workflow status shared by federation connections and links. */\nexport const federationStatus = govcore.enum('federation_status', ['pending', 'active', 'rejected'])\n\n// ── Tenancy root ────────────────────────────────────────────────────────────\n\nexport const organizations = govcore.table(\n 'organizations',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n name: text('name').notNull(),\n slug: text('slug').notNull(),\n /** App-extensible bag for org settings GovCore itself doesn't model. */\n metadata: jsonb('metadata').notNull().default({}),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n },\n (t) => [uniqueIndex('organizations_slug_unique').on(t.slug)],\n)\n\n/**\n * The tenancy column contract (design §5): every tenant-scoped table — core's\n * and the app's own domain tables — carries `organization_id` referencing\n * `organizations`. RLS policies (see ../migrations/0001) key off this column.\n *\n * @example\n * export const permits = pgTable('permits', { ...orgScoped(organizations), title: text('title') })\n */\nexport const orgScoped = (orgs: typeof organizations) => ({\n organizationId: uuid('organization_id')\n .notNull()\n .references((): AnyPgColumn => orgs.id, { onDelete: 'cascade' }),\n})\n\n// ── Identity ────────────────────────────────────────────────────────────────\n\nexport const users = govcore.table(\n 'users',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n organizationId: uuid('organization_id')\n .notNull()\n .references(() => organizations.id, { onDelete: 'cascade' }),\n /** Last-selected active org, honored first by active-membership resolution. */\n lastActiveOrganizationId: uuid('last_active_organization_id').references(\n (): AnyPgColumn => organizations.id,\n { onDelete: 'set null' },\n ),\n name: text('name'),\n email: text('email').notNull(),\n emailVerified: timestamp('email_verified'),\n image: text('image'),\n passwordHash: text('password_hash'),\n /**\n * App-defined role (see @govcore/rbac `createRbac`) — stored as `text`, not\n * a fixed enum, so GovCore ships no role vocabulary of its own. Denormalized\n * cache of the active membership's role; nullable so the Auth.js adapter can\n * create a user before a membership is assigned.\n */\n role: text('role'),\n /** Platform-level role (e.g. instance/platform admin), separate from per-org role. */\n instanceRole: text('instance_role'),\n isActive: boolean('is_active').notNull().default(true),\n // Account-lockout + password-expiry state; the policy itself lives app-side.\n failedLoginAttempts: integer('failed_login_attempts').notNull().default(0),\n lockoutUntil: timestamp('lockout_until'),\n lastPasswordChangedAt: timestamp('last_password_changed_at').notNull().defaultNow(),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n },\n // One identity per email across all orgs — unambiguous auth/SSO lookups.\n (t) => [uniqueIndex('users_email_unique').on(t.email)],\n)\n\n// Auth.js (drizzle-adapter) tables. Not org-scoped; read during auth flows\n// before a tenant context exists, so they are intentionally outside RLS.\n\nexport const accounts = govcore.table('accounts', {\n id: uuid('id').primaryKey().defaultRandom(),\n userId: uuid('user_id')\n .notNull()\n .references(() => users.id, { onDelete: 'cascade' }),\n type: text('type').notNull(),\n provider: text('provider').notNull(),\n providerAccountId: text('provider_account_id').notNull(),\n refreshToken: text('refresh_token'),\n accessToken: text('access_token'),\n expiresAt: timestamp('expires_at'),\n tokenType: text('token_type'),\n scope: text('scope'),\n idToken: text('id_token'),\n sessionState: text('session_state'),\n})\n\nexport const sessions = govcore.table('sessions', {\n id: uuid('id').primaryKey().defaultRandom(),\n sessionToken: text('session_token').notNull().unique(),\n userId: uuid('user_id')\n .notNull()\n .references(() => users.id, { onDelete: 'cascade' }),\n expires: timestamp('expires').notNull(),\n})\n\nexport const verificationTokens = govcore.table('verification_tokens', {\n identifier: text('identifier').notNull(),\n token: text('token').notNull(),\n expires: timestamp('expires').notNull(),\n})\n\n// ── Membership model (the heart of tenancy) ─────────────────────────────────\n\nexport const userOrganizationMemberships = govcore.table(\n 'user_organization_memberships',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n userId: uuid('user_id')\n .notNull()\n .references(() => users.id, { onDelete: 'cascade' }),\n organizationId: uuid('organization_id')\n .notNull()\n .references(() => organizations.id, { onDelete: 'cascade' }),\n /** App-defined role for this membership (text, not an enum — see `users.role`). */\n role: text('role').notNull(),\n /** Revocation soft-deactivates so the historical row survives for audit. */\n isActive: boolean('is_active').notNull().default(true),\n isPrimary: boolean('is_primary').notNull().default(false),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n },\n (t) => [uniqueIndex('user_org_membership_unique').on(t.userId, t.organizationId)],\n)\n\n// ── Audit (append-only; immutability + RLS in ../migrations/0001) ───────────\n\nexport const auditLog = govcore.table('audit_log', {\n id: uuid('id').primaryKey().defaultRandom(),\n organizationId: uuid('organization_id'), // historical UUID — no FK on purpose\n userId: uuid('user_id'), // historical UUID — no FK on purpose\n action: text('action').notNull(),\n entityType: text('entity_type').notNull(),\n entityId: uuid('entity_id'),\n before: jsonb('before'),\n after: jsonb('after'),\n metadata: jsonb('metadata'),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n})\n\n// ── Federation (cross-organization connections and content links) ──────────\n\n/** Explicit bilateral connection between two organizations. */\nexport const orgConnections = govcore.table(\n 'org_connections',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n fromOrgId: uuid('from_org_id')\n .notNull()\n .references(() => organizations.id, { onDelete: 'cascade' }),\n toOrgId: uuid('to_org_id')\n .notNull()\n .references(() => organizations.id, { onDelete: 'cascade' }),\n status: federationStatus('status').notNull().default('pending'),\n createdBy: uuid('created_by').references(() => users.id),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n },\n (t) => [unique('unique_org_connection').on(t.fromOrgId, t.toOrgId)],\n)\n\n/** Approved relationship between content items across orgs. No FK on entity ids\n * (they cross org boundaries); link semantics are app-defined (`link_type` text). */\nexport const crossOrgLinks = govcore.table('cross_org_links', {\n id: uuid('id').primaryKey().defaultRandom(),\n sourceOrgId: uuid('source_org_id')\n .notNull()\n .references(() => organizations.id, { onDelete: 'cascade' }),\n sourceEntityType: text('source_entity_type').notNull(),\n sourceEntityId: uuid('source_entity_id').notNull(),\n targetOrgId: uuid('target_org_id')\n .notNull()\n .references(() => organizations.id, { onDelete: 'cascade' }),\n targetEntityType: text('target_entity_type').notNull(),\n targetEntityId: uuid('target_entity_id').notNull(),\n linkType: text('link_type').notNull(),\n status: federationStatus('status').notNull().default('pending'),\n rejectionReason: text('rejection_reason'),\n flaggedForReview: boolean('flagged_for_review').notNull().default(false),\n flagReason: text('flag_reason'),\n createdBy: uuid('created_by').references(() => users.id),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n})\n\n// ── Support access (break-glass + act-as) ──────────────────────────────────\n// Instance-operator constructs that deliberately cross the tenant boundary, so\n// they are NOT under the org-GUC RLS (the actor is an instance admin operating\n// across orgs). Authorization is enforced in @govcore/support (design §6.6).\n\nexport const breakGlassSessions = govcore.table('break_glass_sessions', {\n id: uuid('id').primaryKey().defaultRandom(),\n instanceAdminId: uuid('instance_admin_id')\n .notNull()\n .references(() => users.id),\n targetOrgId: uuid('target_org_id')\n .notNull()\n .references(() => organizations.id),\n reason: text('reason').notNull(),\n grantedAt: timestamp('granted_at').notNull().defaultNow(),\n expiresAt: timestamp('expires_at').notNull(),\n requiresApproval: boolean('requires_approval').notNull().default(false),\n approvedAt: timestamp('approved_at'),\n approvedBy: uuid('approved_by').references(() => users.id),\n revokedAt: timestamp('revoked_at'),\n revokedBy: uuid('revoked_by').references(() => users.id),\n})\n\nexport const actAsSessions = govcore.table('act_as_sessions', {\n id: uuid('id').primaryKey().defaultRandom(),\n breakGlassSessionId: uuid('break_glass_session_id')\n .notNull()\n .references(() => breakGlassSessions.id),\n instanceAdminId: uuid('instance_admin_id')\n .notNull()\n .references(() => users.id),\n targetOrgId: uuid('target_org_id')\n .notNull()\n .references(() => organizations.id),\n startedAt: timestamp('started_at').notNull().defaultNow(),\n expiresAt: timestamp('expires_at').notNull(),\n endedAt: timestamp('ended_at'),\n endReason: text('end_reason'),\n})\n\nexport const ACT_AS_DEFAULT_TTL_MINUTES = 30\nexport const ACT_AS_END_REASONS = [\n 'admin_ended',\n 'expired',\n 'parent_revoked',\n 'parent_expired',\n] as const\nexport type ActAsEndReason = (typeof ACT_AS_END_REASONS)[number]\n\n// ── Instance / platform configuration (singletons; not org-scoped) ─────────\n\nexport const instanceSettings = govcore.table('instance_settings', {\n id: uuid('id').primaryKey().defaultRandom(),\n disabledModules: jsonb('disabled_modules').$type<Record<string, boolean>>().notNull().default({}),\n createdAt: timestamp('created_at').notNull().defaultNow(),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n})\n\nexport const platformConfig = govcore.table('platform_config', {\n id: text('id').primaryKey().default('singleton'),\n instanceName: text('instance_name').notNull().default('GovCore'),\n defaultTheme: text('default_theme').notNull().default('base'),\n allowLocalAuth: boolean('allow_local_auth').notNull().default(true),\n /** Stamped onto new orgs at provisioning time. Null means no tier. */\n defaultSupportTier: text('default_support_tier'),\n updatedAt: timestamp('updated_at').notNull().defaultNow(),\n updatedBy: uuid('updated_by').references(() => users.id, { onDelete: 'set null' }),\n})\n\n// ── Inferred types ──────────────────────────────────────────────────────────\n\nexport type Organization = typeof organizations.$inferSelect\nexport type NewOrganization = typeof organizations.$inferInsert\nexport type User = typeof users.$inferSelect\nexport type NewUser = typeof users.$inferInsert\nexport type UserOrganizationMembership = typeof userOrganizationMemberships.$inferSelect\nexport type NewUserOrganizationMembership = typeof userOrganizationMemberships.$inferInsert\nexport type AuditEntry = typeof auditLog.$inferSelect\nexport type NewAuditEntry = typeof auditLog.$inferInsert\nexport type OrgConnection = typeof orgConnections.$inferSelect\nexport type NewOrgConnection = typeof orgConnections.$inferInsert\nexport type CrossOrgLink = typeof crossOrgLinks.$inferSelect\nexport type NewCrossOrgLink = typeof crossOrgLinks.$inferInsert\nexport type BreakGlassSession = typeof breakGlassSessions.$inferSelect\nexport type NewBreakGlassSession = typeof breakGlassSessions.$inferInsert\nexport type ActAsSession = typeof actAsSessions.$inferSelect\nexport type NewActAsSession = typeof actAsSessions.$inferInsert\nexport type InstanceSettings = typeof instanceSettings.$inferSelect\nexport type NewInstanceSettings = typeof instanceSettings.$inferInsert\nexport type PlatformConfig = typeof platformConfig.$inferSelect\nexport type NewPlatformConfig = typeof platformConfig.$inferInsert\n","// @govcore/schema — platform table definitions + schema version.\n// Edge-safe entrypoint (no DB client). The migrate runner is ./migrate.\n\nimport type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core'\n\nexport * from './schema'\n\n/**\n * A Drizzle Postgres database **or** transaction handle. Functions in the\n * tenancy/audit packages accept this so callers inject their own client (the\n * app's `postgres-js` db, or a `tx` inside a transaction — which extends it).\n */\nexport type GovcoreDb = PgDatabase<PgQueryResultHKT, any, any>\n\n/**\n * True when `err` is (or wraps) a Postgres unique-constraint violation\n * (SQLSTATE 23505). Pure and edge-safe — inspects `code`, touches no DB client.\n * Lets operator flows turn a duplicate slug/email into a typed result instead of\n * a 500. Walks the `cause` chain because Drizzle wraps the driver error in a\n * `DrizzleQueryError` and puts the `postgres-js`/`pg` error (which carries the\n * `code`) on `.cause` — so the top-level error's own `code` is undefined.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n for (let e: unknown = err, depth = 0; e && typeof e === 'object' && depth < 5; depth++) {\n if ((e as { code?: string }).code === '23505') return true\n e = (e as { cause?: unknown }).cause\n }\n return false\n}\n\n/**\n * Bumped whenever the platform schema changes. Written to instance settings on\n * boot for observability (\"this instance runs platform schema vN\", design §5),\n * and stamped into backup archives (design §13.5). Not load-bearing for\n * correctness — the migrations are — but useful for diagnostics.\n */\nexport const CORE_SCHEMA_VERSION = '0.0.0'\n"],"mappings":"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","names":[]}
package/dist/migrate.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- export interface MigrateOptions {
1
+ interface MigrateOptions {
2
2
  /** Connection string for the owner/DDL role. Defaults to env. */
3
3
  connectionString?: string;
4
4
  /** Optional logger; defaults to console.log. */
5
5
  log?: (message: string) => void;
6
6
  }
7
7
  /** Apply all pending platform migrations. Idempotent — already-applied files are skipped. */
8
- export declare function migrate(options?: MigrateOptions): Promise<{
8
+ declare function migrate(options?: MigrateOptions): Promise<{
9
9
  applied: string[];
10
10
  }>;
11
- //# sourceMappingURL=migrate.d.ts.map
11
+
12
+ export { type MigrateOptions, migrate };
package/dist/migrate.js CHANGED
@@ -1,64 +1,56 @@
1
- // @govcore/schema/migrate — the govcore-migrate runner.
2
- //
3
- // NOT edge-safe (uses node:fs + a postgres client). Applies the authored
4
- // platform migrations in ../migrations in lexical order, each in its own
5
- // transaction, tracked in `govcore.__govcore_migrations` — kept separate from
6
- // the app's own Drizzle journal so the two migration streams never overlap
7
- // (design §5).
8
- //
9
- // Runs as the OWNER / DDL role (design §13.2): set GOVCORE_MIGRATE_DATABASE_URL
10
- // to the owning role's connection string; falls back to DATABASE_URL. The app
11
- // runtime connects as a separate NON-OWNER role, which RLS binds.
12
- import { readdirSync, readFileSync } from 'node:fs';
13
- import { dirname, join } from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
- import postgres from 'postgres';
16
- const MIGRATIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations');
17
- /** Apply all pending platform migrations. Idempotent — already-applied files are skipped. */
18
- export async function migrate(options = {}) {
19
- const connectionString = options.connectionString ??
20
- process.env.GOVCORE_MIGRATE_DATABASE_URL ??
21
- process.env.DATABASE_URL;
22
- if (!connectionString) {
23
- throw new Error('govcore-migrate: set GOVCORE_MIGRATE_DATABASE_URL (owner role) or DATABASE_URL');
24
- }
25
- const log = options.log ?? ((m) => console.log(m));
26
- const sql = postgres(connectionString, { max: 1 });
27
- const applied = [];
28
- try {
29
- await sql.unsafe('CREATE SCHEMA IF NOT EXISTS govcore');
30
- await sql.unsafe(`CREATE TABLE IF NOT EXISTS govcore.__govcore_migrations (
1
+ // src/migrate.ts
2
+ import { readdirSync, readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import postgres from "postgres";
6
+ var MIGRATIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "migrations");
7
+ async function migrate(options = {}) {
8
+ const connectionString = options.connectionString ?? process.env.GOVCORE_MIGRATE_DATABASE_URL ?? process.env.DATABASE_URL;
9
+ if (!connectionString) {
10
+ throw new Error(
11
+ "govcore-migrate: set GOVCORE_MIGRATE_DATABASE_URL (owner role) or DATABASE_URL"
12
+ );
13
+ }
14
+ const log = options.log ?? ((m) => console.log(m));
15
+ const sql = postgres(connectionString, { max: 1 });
16
+ const applied = [];
17
+ try {
18
+ await sql.unsafe("CREATE SCHEMA IF NOT EXISTS govcore");
19
+ await sql.unsafe(
20
+ `CREATE TABLE IF NOT EXISTS govcore.__govcore_migrations (
31
21
  name text PRIMARY KEY,
32
22
  applied_at timestamptz NOT NULL DEFAULT now()
33
- )`);
34
- const done = new Set((await sql `SELECT name FROM govcore.__govcore_migrations`).map((r) => r.name));
35
- const files = readdirSync(MIGRATIONS_DIR)
36
- .filter((f) => f.endsWith('.sql'))
37
- .sort();
38
- for (const file of files) {
39
- if (done.has(file))
40
- continue;
41
- const ddl = readFileSync(join(MIGRATIONS_DIR, file), 'utf8');
42
- await sql.begin(async (tx) => {
43
- await tx.unsafe(ddl);
44
- await tx `INSERT INTO govcore.__govcore_migrations (name) VALUES (${file})`;
45
- });
46
- applied.push(file);
47
- log(`govcore-migrate: applied ${file}`);
48
- }
49
- log(applied.length
50
- ? `govcore-migrate: applied ${applied.length} migration(s)`
51
- : 'govcore-migrate: already up to date');
52
- return { applied };
53
- }
54
- finally {
55
- await sql.end();
23
+ )`
24
+ );
25
+ const done = new Set(
26
+ (await sql`SELECT name FROM govcore.__govcore_migrations`).map((r) => r.name)
27
+ );
28
+ const files = readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith(".sql")).sort();
29
+ for (const file of files) {
30
+ if (done.has(file)) continue;
31
+ const ddl = readFileSync(join(MIGRATIONS_DIR, file), "utf8");
32
+ await sql.begin(async (tx) => {
33
+ await tx.unsafe(ddl);
34
+ await tx`INSERT INTO govcore.__govcore_migrations (name) VALUES (${file})`;
35
+ });
36
+ applied.push(file);
37
+ log(`govcore-migrate: applied ${file}`);
56
38
  }
39
+ log(
40
+ applied.length ? `govcore-migrate: applied ${applied.length} migration(s)` : "govcore-migrate: already up to date"
41
+ );
42
+ return { applied };
43
+ } finally {
44
+ await sql.end();
45
+ }
57
46
  }
58
- // Run when invoked as the `govcore-migrate` bin.
59
47
  if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
60
- migrate().catch((err) => {
61
- console.error(err);
62
- process.exit(1);
63
- });
48
+ migrate().catch((err) => {
49
+ console.error(err);
50
+ process.exit(1);
51
+ });
64
52
  }
53
+ export {
54
+ migrate
55
+ };
56
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["// @govcore/schema/migrate — the govcore-migrate runner.\n//\n// NOT edge-safe (uses node:fs + a postgres client). Applies the authored\n// platform migrations in ../migrations in lexical order, each in its own\n// transaction, tracked in `govcore.__govcore_migrations` — kept separate from\n// the app's own Drizzle journal so the two migration streams never overlap\n// (design §5).\n//\n// Runs as the OWNER / DDL role (design §13.2): set GOVCORE_MIGRATE_DATABASE_URL\n// to the owning role's connection string; falls back to DATABASE_URL. The app\n// runtime connects as a separate NON-OWNER role, which RLS binds.\n\nimport { readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport postgres from 'postgres'\n\nconst MIGRATIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations')\n\nexport interface MigrateOptions {\n /** Connection string for the owner/DDL role. Defaults to env. */\n connectionString?: string\n /** Optional logger; defaults to console.log. */\n log?: (message: string) => void\n}\n\n/** Apply all pending platform migrations. Idempotent — already-applied files are skipped. */\nexport async function migrate(options: MigrateOptions = {}): Promise<{ applied: string[] }> {\n const connectionString =\n options.connectionString ??\n process.env.GOVCORE_MIGRATE_DATABASE_URL ??\n process.env.DATABASE_URL\n if (!connectionString) {\n throw new Error(\n 'govcore-migrate: set GOVCORE_MIGRATE_DATABASE_URL (owner role) or DATABASE_URL',\n )\n }\n const log = options.log ?? ((m: string) => console.log(m))\n const sql = postgres(connectionString, { max: 1 })\n const applied: string[] = []\n try {\n await sql.unsafe('CREATE SCHEMA IF NOT EXISTS govcore')\n await sql.unsafe(\n `CREATE TABLE IF NOT EXISTS govcore.__govcore_migrations (\n name text PRIMARY KEY,\n applied_at timestamptz NOT NULL DEFAULT now()\n )`,\n )\n\n const done = new Set(\n (await sql`SELECT name FROM govcore.__govcore_migrations`).map((r) => r.name as string),\n )\n const files = readdirSync(MIGRATIONS_DIR)\n .filter((f) => f.endsWith('.sql'))\n .sort()\n\n for (const file of files) {\n if (done.has(file)) continue\n const ddl = readFileSync(join(MIGRATIONS_DIR, file), 'utf8')\n await sql.begin(async (tx) => {\n await tx.unsafe(ddl)\n await tx`INSERT INTO govcore.__govcore_migrations (name) VALUES (${file})`\n })\n applied.push(file)\n log(`govcore-migrate: applied ${file}`)\n }\n\n log(\n applied.length\n ? `govcore-migrate: applied ${applied.length} migration(s)`\n : 'govcore-migrate: already up to date',\n )\n return { applied }\n } finally {\n await sql.end()\n }\n}\n\n// Run when invoked as the `govcore-migrate` bin.\nif (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {\n migrate().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n"],"mappings":";AAYA,SAAS,aAAa,oBAAoB;AAC1C,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAC9B,OAAO,cAAc;AAErB,IAAM,iBAAiB,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,MAAM,YAAY;AAUvF,eAAsB,QAAQ,UAA0B,CAAC,GAAmC;AAC1F,QAAM,mBACJ,QAAQ,oBACR,QAAQ,IAAI,gCACZ,QAAQ,IAAI;AACd,MAAI,CAAC,kBAAkB;AACrB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,MAAM,QAAQ,QAAQ,CAAC,MAAc,QAAQ,IAAI,CAAC;AACxD,QAAM,MAAM,SAAS,kBAAkB,EAAE,KAAK,EAAE,CAAC;AACjD,QAAM,UAAoB,CAAC;AAC3B,MAAI;AACF,UAAM,IAAI,OAAO,qCAAqC;AACtD,UAAM,IAAI;AAAA,MACR;AAAA;AAAA;AAAA;AAAA,IAIF;AAEA,UAAM,OAAO,IAAI;AAAA,OACd,MAAM,oDAAoD,IAAI,CAAC,MAAM,EAAE,IAAc;AAAA,IACxF;AACA,UAAM,QAAQ,YAAY,cAAc,EACrC,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,CAAC,EAChC,KAAK;AAER,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,YAAM,MAAM,aAAa,KAAK,gBAAgB,IAAI,GAAG,MAAM;AAC3D,YAAM,IAAI,MAAM,OAAO,OAAO;AAC5B,cAAM,GAAG,OAAO,GAAG;AACnB,cAAM,6DAA6D,IAAI;AAAA,MACzE,CAAC;AACD,cAAQ,KAAK,IAAI;AACjB,UAAI,4BAA4B,IAAI,EAAE;AAAA,IACxC;AAEA;AAAA,MACE,QAAQ,SACJ,4BAA4B,QAAQ,MAAM,kBAC1C;AAAA,IACN;AACA,WAAO,EAAE,QAAQ;AAAA,EACnB,UAAE;AACA,UAAM,IAAI,IAAI;AAAA,EAChB;AACF;AAGA,IAAI,QAAQ,KAAK,CAAC,KAAK,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,IAAI;AACtE,UAAQ,EAAE,MAAM,CAAC,QAAQ;AACvB,YAAQ,MAAM,GAAG;AACjB,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;","names":[]}
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@govcore/schema",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Platform tables, enums, migrations, and the govcore-migrate runner",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.ts",
11
- "default": "./src/index.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
12
  },
13
13
  "./migrate": {
14
- "types": "./src/migrate.ts",
15
- "default": "./src/migrate.ts"
14
+ "types": "./dist/migrate.d.ts",
15
+ "default": "./dist/migrate.js"
16
16
  }
17
17
  },
18
18
  "bin": {
@@ -26,10 +26,28 @@
26
26
  "postgres": "^3.4.0"
27
27
  },
28
28
  "devDependencies": {
29
- "typescript": "^5.5.0"
29
+ "typescript": "^5.5.0",
30
+ "tsup": "^8.3.0"
31
+ },
32
+ "module": "./dist/index.js",
33
+ "files": [
34
+ "dist",
35
+ "migrations"
36
+ ],
37
+ "tsup": {
38
+ "entry": [
39
+ "src/index.ts",
40
+ "src/migrate.ts"
41
+ ],
42
+ "format": [
43
+ "esm"
44
+ ],
45
+ "dts": true,
46
+ "clean": true,
47
+ "sourcemap": true
30
48
  },
31
49
  "scripts": {
32
- "build": "tsc -p tsconfig.json",
33
- "dev": "tsc -p tsconfig.json --watch"
50
+ "build": "tsup",
51
+ "dev": "tsup --watch"
34
52
  }
35
53
  }
package/CHANGELOG.md DELETED
@@ -1,34 +0,0 @@
1
- # @govcore/schema
2
-
3
- ## 0.2.0
4
-
5
- ### Minor Changes
6
-
7
- - d255afc: Operator-plane console mutations (#63). The org/user administration flows behind an instance console were rebuilt by every consumer (GovEA `actions/instance.ts`, GovCRM `lib/platform.ts`), and the user path is exactly where they diverged. Promote the mutations to core, composing the membership invariants from #65 so the guard and write-sync are identical everywhere.
8
-
9
- `@govcore/tenancy` gains `createOrganization` (auto-slug via the new exported `slugify`; a duplicate slug returns a typed `slug-taken`), `renameOrganization` (name-only; audited before/after), and `updateUserAdministration` — the guard-heavy one: it enforces the last-active-admin invariant via `assertNotLastActiveAdmin` (inside the transaction) and an own-instance-admin lockout, updates the `users` row, and keeps the membership in lockstep via `upsertMembership`. All audited as `platform.org.*` / `platform.user.update` and generic over the app's admin role name; tenancy now depends on `@govcore/audit`.
10
-
11
- `@govcore/auth` gains `provisionUser` — create a user with an initial password + primary membership. It lives here (not tenancy) because it hashes: validates against the policy, hashes, inserts, writes the membership through tenancy's `upsertMembership`, and audits `platform.user.create` without ever putting the password in the payload; a duplicate email returns a typed `email-taken`.
12
-
13
- `@govcore/schema` gains `isUniqueViolation(err)` — a pure, edge-safe SQLSTATE-23505 predicate so operator flows turn a duplicate slug/email into a typed result instead of a 500.
14
-
15
- All framework-agnostic (no FormData/redirect/revalidate — the consumer keeps the thin `'use server'` wrapper and the `instance_admin` gate) and returning typed results rather than throwing.
16
-
17
- ## 0.1.0
18
-
19
- ### Minor Changes
20
-
21
- - f2f3743: Add `@govcore/schema`: the platform schema (identity, tenancy, auth, audit) in a
22
- dedicated `govcore` Postgres schema, with the `orgScoped` tenancy-column helper
23
- and `CORE_SCHEMA_VERSION`. Roles are `text` (app-defined via `@govcore/rbac`),
24
- not a fixed enum.
25
-
26
- Also adds federation (`org_connections`, `cross_org_links`), support access
27
- (`break_glass_sessions`, `act_as_sessions`), and instance config
28
- (`instance_settings`, `platform_config`).
29
-
30
- Ships authored migrations (`0000_platform_init`, `0001_platform_security` —
31
- append-only audit trigger + Row-Level Security with `FORCE ROW LEVEL SECURITY`;
32
- `0002_platform_federation_support` — federation tables with a both-participant
33
- RLS check) and the `govcore-migrate` runner (separate, non-edge `./migrate`
34
- entrypoint; tracked in `govcore.__govcore_migrations`; runs as the owner/DDL role).
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEvE,cAAc,UAAU,CAAA;AAExB;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,gBAAgB,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAE9D;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMvD;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,UAAU,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gDAAgD;IAChD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAED,6FAA6F;AAC7F,wBAAsB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAiD1F"}