@willyim/idp 0.1.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 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/drizzle/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,aAAa,CAAA;AAE9D,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAE9C;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,MAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBrE;AAED,uDAAuD;AACvD,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBjE;AAED,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAA0B,CAAA;AAEjD;;;;GAIG;AACH,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IAC/B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IAC/B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;IAC/B,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAA;CAChC,CAAA;AAED,KAAK,eAAe,GAChB,UAAU,CAAC,OAAO,qBAAqB,CAAC,GACxC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAExC,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,YAAY,CA8BzF"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @willyim/idp/drizzle — the one table a consumer app owns.
3
+ *
4
+ * Import the table into your schema, run `drizzle-kit generate`, and hand the
5
+ * pair to `createIdp({ sessions: drizzleSessions(db, schema.idpSession) })`.
6
+ * That is the whole migration story: one table, no user table, no account
7
+ * table, no verification table.
8
+ *
9
+ * `drizzle-orm` is an optional peer dependency of this subpath only — core
10
+ * never imports it.
11
+ */
12
+ import { eq } from "drizzle-orm";
13
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
14
+ import { jsonb, pgTable, text as pgText, timestamp } from "drizzle-orm/pg-core";
15
+ export const IDP_SESSION_TABLE = "idp_session";
16
+ /**
17
+ * SQLite / Cloudflare D1. Timestamps are epoch-millisecond integers and the
18
+ * claim columns are JSON text — both are what drizzle's own modes produce, so
19
+ * `drizzle-kit generate` needs no help.
20
+ */
21
+ export function idpSessionSqliteTable(name = IDP_SESSION_TABLE) {
22
+ return sqliteTable(name, {
23
+ id: text("id").primaryKey(),
24
+ sub: text("sub").notNull(),
25
+ email: text("email").notNull(),
26
+ name: text("name"),
27
+ image: text("image"),
28
+ permissions: text("permissions", { mode: "json" }).$type().notNull(),
29
+ workspaces: text("workspaces", { mode: "json" }).$type().notNull(),
30
+ actor: text("actor", { mode: "json" }).$type(),
31
+ accessToken: text("access_token").notNull(),
32
+ refreshToken: text("refresh_token"),
33
+ idToken: text("id_token"),
34
+ accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp_ms" }),
35
+ syncedAt: integer("synced_at", { mode: "timestamp_ms" }).notNull(),
36
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
37
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
38
+ });
39
+ }
40
+ /** Postgres. Same columns, `jsonb` + `timestamptz`. */
41
+ export function idpSessionPgTable(name = IDP_SESSION_TABLE) {
42
+ return pgTable(name, {
43
+ id: pgText("id").primaryKey(),
44
+ sub: pgText("sub").notNull(),
45
+ email: pgText("email").notNull(),
46
+ name: pgText("name"),
47
+ image: pgText("image"),
48
+ permissions: jsonb("permissions").$type().notNull(),
49
+ workspaces: jsonb("workspaces").$type().notNull(),
50
+ actor: jsonb("actor").$type(),
51
+ accessToken: pgText("access_token").notNull(),
52
+ refreshToken: pgText("refresh_token"),
53
+ idToken: pgText("id_token"),
54
+ accessTokenExpiresAt: timestamp("access_token_expires_at", { withTimezone: true }),
55
+ syncedAt: timestamp("synced_at", { withTimezone: true }).notNull(),
56
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
57
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
58
+ });
59
+ }
60
+ /** The SQLite/D1 table under its default name — what most apps import. */
61
+ export const idpSession = idpSessionSqliteTable();
62
+ /** A `SessionStore` backed by drizzle. Works with D1/SQLite and Postgres alike. */
63
+ export function drizzleSessions(db, table) {
64
+ const t = table;
65
+ async function get(id) {
66
+ const rows = await db.select().from(t).where(eq(t.id, id)).limit(1);
67
+ return rows[0] ? toRecord(rows[0]) : null;
68
+ }
69
+ return {
70
+ get,
71
+ async create(record) {
72
+ await db.insert(t).values(record);
73
+ return record;
74
+ },
75
+ async update(id, patch) {
76
+ const values = Object.fromEntries(Object.entries(patch).filter(([key, value]) => key !== "id" && value !== undefined));
77
+ if (Object.keys(values).length > 0) {
78
+ await db.update(t).set(values).where(eq(t.id, id));
79
+ }
80
+ return get(id);
81
+ },
82
+ async delete(id) {
83
+ await db.delete(t).where(eq(t.id, id));
84
+ },
85
+ async deleteBySub(sub) {
86
+ await db.delete(t).where(eq(t.sub, sub));
87
+ },
88
+ };
89
+ }
90
+ /**
91
+ * Drivers disagree about how faithfully they round-trip a `Date`, so coerce on
92
+ * the way out rather than trusting the dialect.
93
+ */
94
+ function toRecord(row) {
95
+ return {
96
+ ...row,
97
+ permissions: asArray(row.permissions),
98
+ workspaces: asArray(row.workspaces),
99
+ actor: parseJson(row.actor) ?? null,
100
+ accessTokenExpiresAt: asDate(row.accessTokenExpiresAt),
101
+ syncedAt: asDate(row.syncedAt) ?? new Date(0),
102
+ expiresAt: asDate(row.expiresAt) ?? new Date(0),
103
+ createdAt: asDate(row.createdAt) ?? new Date(0),
104
+ };
105
+ }
106
+ function parseJson(value) {
107
+ if (typeof value !== "string")
108
+ return value;
109
+ try {
110
+ return JSON.parse(value);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function asArray(value) {
117
+ const parsed = parseJson(value);
118
+ return Array.isArray(parsed) ? parsed : [];
119
+ }
120
+ function asDate(value) {
121
+ if (value === null || value === undefined)
122
+ return null;
123
+ if (value instanceof Date)
124
+ return value;
125
+ if (typeof value === "number" || typeof value === "string") {
126
+ const date = new Date(value);
127
+ return Number.isNaN(date.getTime()) ? null : date;
128
+ }
129
+ return null;
130
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `"7d"` -> milliseconds. Config reads better as a duration string than as a
3
+ * number of milliseconds nobody can eyeball; numbers are still accepted and
4
+ * treated as milliseconds.
5
+ */
6
+ export type Duration = number | `${number}${"ms" | "s" | "m" | "h" | "d" | "w"}`;
7
+ export declare function parseDuration(value: Duration): number;
8
+ //# sourceMappingURL=duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../src/duration.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,GAAG,MAAM,GAAG,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,CAAA;AAWhF,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAQrD"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `"7d"` -> milliseconds. Config reads better as a duration string than as a
3
+ * number of milliseconds nobody can eyeball; numbers are still accepted and
4
+ * treated as milliseconds.
5
+ */
6
+ const UNITS = {
7
+ ms: 1,
8
+ s: 1000,
9
+ m: 60_000,
10
+ h: 3_600_000,
11
+ d: 86_400_000,
12
+ w: 604_800_000,
13
+ };
14
+ export function parseDuration(value) {
15
+ if (typeof value === "number") {
16
+ if (!Number.isFinite(value) || value < 0)
17
+ throw new Error(`invalid duration: ${value}`);
18
+ return value;
19
+ }
20
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(value.trim());
21
+ if (!match)
22
+ throw new Error(`invalid duration: ${value}`);
23
+ return Number(match[1]) * UNITS[match[2]];
24
+ }