okengine 0.17.2 → 0.18.3
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/README.md +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL persist / hydrate for {@link UserIdentityRow} + {@link UserAccountRow}.
|
|
3
|
+
*
|
|
4
|
+
* Host path: public schema on the app `store.sql()` connection
|
|
5
|
+
* (`sharedSqlConn` / `DATABASE_URL`), mirroring `api-key-sql.ts`.
|
|
6
|
+
* The identity store hydrates `oke_identities` / `oke_credentials` at host
|
|
7
|
+
* boot and write-through-persists every user/account row thereafter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SqlConnection } from "../drivers/types.ts";
|
|
11
|
+
import { AUTH_TABLES } from "./tables.ts";
|
|
12
|
+
import type { IdentityStore, UserAccountRow, UserIdentityRow } from "./identity.ts";
|
|
13
|
+
|
|
14
|
+
/** Minimal SQL executor for the identity tables. */
|
|
15
|
+
export interface IdentitySqlExec {
|
|
16
|
+
execute(sql: string, params: readonly unknown[]): Promise<void>;
|
|
17
|
+
all(sql: string): Promise<readonly Record<string, unknown>[]>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const USER_COLUMNS = "id, email, name, email_verified, status, created_at, updated_at, extra_json";
|
|
21
|
+
const ACCOUNT_COLUMNS =
|
|
22
|
+
"id, user_id, provider, provider_account_id, password_hash, created_at, updated_at";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Persist one user row to `oke_identities`.
|
|
26
|
+
*
|
|
27
|
+
* @param sql - Executor
|
|
28
|
+
* @param row - User row
|
|
29
|
+
* @param table - Physical table
|
|
30
|
+
*/
|
|
31
|
+
export async function persistIdentityUserRow(
|
|
32
|
+
sql: IdentitySqlExec,
|
|
33
|
+
row: UserIdentityRow,
|
|
34
|
+
table: string = AUTH_TABLES.identities,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
await sql.execute(
|
|
37
|
+
`INSERT INTO ${table} (${USER_COLUMNS})
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
39
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
40
|
+
email = excluded.email,
|
|
41
|
+
name = excluded.name,
|
|
42
|
+
email_verified = excluded.email_verified,
|
|
43
|
+
status = excluded.status,
|
|
44
|
+
created_at = excluded.created_at,
|
|
45
|
+
updated_at = excluded.updated_at,
|
|
46
|
+
extra_json = excluded.extra_json`,
|
|
47
|
+
[
|
|
48
|
+
row.id,
|
|
49
|
+
row.email,
|
|
50
|
+
row.name,
|
|
51
|
+
row.emailVerified ? 1 : 0,
|
|
52
|
+
row.status,
|
|
53
|
+
row.createdAt,
|
|
54
|
+
row.updatedAt,
|
|
55
|
+
JSON.stringify(row.extra),
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Persist one account row to `oke_credentials`.
|
|
62
|
+
*
|
|
63
|
+
* @param sql - Executor
|
|
64
|
+
* @param row - Account row
|
|
65
|
+
* @param table - Physical table
|
|
66
|
+
*/
|
|
67
|
+
export async function persistIdentityAccountRow(
|
|
68
|
+
sql: IdentitySqlExec,
|
|
69
|
+
row: UserAccountRow,
|
|
70
|
+
table: string = AUTH_TABLES.credentials,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
await sql.execute(
|
|
73
|
+
`INSERT INTO ${table} (${ACCOUNT_COLUMNS})
|
|
74
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
75
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
76
|
+
user_id = excluded.user_id,
|
|
77
|
+
provider = excluded.provider,
|
|
78
|
+
provider_account_id = excluded.provider_account_id,
|
|
79
|
+
password_hash = excluded.password_hash,
|
|
80
|
+
created_at = excluded.created_at,
|
|
81
|
+
updated_at = excluded.updated_at`,
|
|
82
|
+
[
|
|
83
|
+
row.id,
|
|
84
|
+
row.userId,
|
|
85
|
+
row.provider,
|
|
86
|
+
row.providerAccountId,
|
|
87
|
+
row.passwordHash,
|
|
88
|
+
row.createdAt,
|
|
89
|
+
row.updatedAt,
|
|
90
|
+
],
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Bind write-through persist hooks on a store.
|
|
96
|
+
*
|
|
97
|
+
* @param store - Identity store
|
|
98
|
+
* @param sql - Executor
|
|
99
|
+
* @param userTable - Physical `oke_identities`
|
|
100
|
+
* @param accountTable - Physical `oke_credentials`
|
|
101
|
+
*/
|
|
102
|
+
export function bindIdentitySqlPersist(
|
|
103
|
+
store: IdentityStore,
|
|
104
|
+
sql: IdentitySqlExec,
|
|
105
|
+
userTable: string = AUTH_TABLES.identities,
|
|
106
|
+
accountTable: string = AUTH_TABLES.credentials,
|
|
107
|
+
): void {
|
|
108
|
+
store.persistUser = (row) => persistIdentityUserRow(sql, row, userTable);
|
|
109
|
+
store.persistAccount = (row) => persistIdentityAccountRow(sql, row, accountTable);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Load every identity + credential row into a store, rebuilding derived maps.
|
|
114
|
+
*
|
|
115
|
+
* @param sql - Executor
|
|
116
|
+
* @param store - Destination store
|
|
117
|
+
* @param userTable - Physical `oke_identities`
|
|
118
|
+
* @param accountTable - Physical `oke_credentials`
|
|
119
|
+
*/
|
|
120
|
+
export async function hydrateIdentityStore(
|
|
121
|
+
sql: IdentitySqlExec,
|
|
122
|
+
store: IdentityStore,
|
|
123
|
+
userTable: string = AUTH_TABLES.identities,
|
|
124
|
+
accountTable: string = AUTH_TABLES.credentials,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const users = await sql.all(`SELECT * FROM ${userTable}`);
|
|
127
|
+
for (const raw of users) {
|
|
128
|
+
const row = userFromSql(raw);
|
|
129
|
+
if (!row) continue;
|
|
130
|
+
store.users.set(row.id, row);
|
|
131
|
+
if (row.email) store.byEmail.set(normalizeKey(row.email), row.id);
|
|
132
|
+
}
|
|
133
|
+
const accounts = await sql.all(`SELECT * FROM ${accountTable}`);
|
|
134
|
+
for (const raw of accounts) {
|
|
135
|
+
const row = accountFromSql(raw);
|
|
136
|
+
if (!row) continue;
|
|
137
|
+
store.accounts.set(row.id, row);
|
|
138
|
+
store.byProvider.set(`${row.provider}:${row.providerAccountId}`, row.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create public `oke_identities` / `oke_credentials` when missing.
|
|
144
|
+
*
|
|
145
|
+
* @param conn - Shared SQL connection
|
|
146
|
+
* @param userTable - Physical table
|
|
147
|
+
* @param accountTable - Physical table
|
|
148
|
+
*/
|
|
149
|
+
export async function ensureIdentityTables(
|
|
150
|
+
conn: SqlConnection,
|
|
151
|
+
userTable: string = AUTH_TABLES.identities,
|
|
152
|
+
accountTable: string = AUTH_TABLES.credentials,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
await conn.exec(
|
|
155
|
+
`CREATE TABLE IF NOT EXISTS ${userTable} (
|
|
156
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
157
|
+
email TEXT NOT NULL,
|
|
158
|
+
name TEXT NOT NULL,
|
|
159
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
160
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
161
|
+
created_at BIGINT NOT NULL,
|
|
162
|
+
updated_at BIGINT NOT NULL,
|
|
163
|
+
extra_json TEXT NOT NULL DEFAULT '{}'
|
|
164
|
+
)`,
|
|
165
|
+
);
|
|
166
|
+
await conn.exec(
|
|
167
|
+
`CREATE TABLE IF NOT EXISTS ${accountTable} (
|
|
168
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
169
|
+
user_id TEXT NOT NULL,
|
|
170
|
+
provider TEXT NOT NULL,
|
|
171
|
+
provider_account_id TEXT NOT NULL,
|
|
172
|
+
password_hash TEXT,
|
|
173
|
+
created_at BIGINT NOT NULL,
|
|
174
|
+
updated_at BIGINT NOT NULL
|
|
175
|
+
)`,
|
|
176
|
+
);
|
|
177
|
+
await conn.exec(
|
|
178
|
+
`CREATE INDEX IF NOT EXISTS ${accountTable}_provider_idx ON ${accountTable} (provider, provider_account_id)`,
|
|
179
|
+
);
|
|
180
|
+
await conn.exec(
|
|
181
|
+
`CREATE INDEX IF NOT EXISTS ${accountTable}_user_idx ON ${accountTable} (user_id)`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Runtime that can open the app's shared primary SQL connection. */
|
|
186
|
+
export interface HostIdentitySqlRuntime {
|
|
187
|
+
primarySql(): Promise<SqlConnection | undefined>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Adapt a driver connection to {@link IdentitySqlExec}.
|
|
192
|
+
*
|
|
193
|
+
* @param conn - Shared `store.sql()` connection
|
|
194
|
+
*/
|
|
195
|
+
export function identitySqlExec(conn: SqlConnection): IdentitySqlExec {
|
|
196
|
+
return {
|
|
197
|
+
execute: async (sql, params) => {
|
|
198
|
+
await conn.exec(sql, params);
|
|
199
|
+
},
|
|
200
|
+
all: async (sql) => conn.query(sql),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hydrate and write-through-persist a host {@link IdentityStore} on `conn`.
|
|
206
|
+
*
|
|
207
|
+
* Leaves already-bound `persist` hooks in place (injected test stores).
|
|
208
|
+
*
|
|
209
|
+
* @param conn - App SQL connection (`sharedSqlConn`)
|
|
210
|
+
* @param store - `gate.auth.identities`
|
|
211
|
+
*/
|
|
212
|
+
export async function bindHostIdentitySql(
|
|
213
|
+
conn: SqlConnection,
|
|
214
|
+
store: IdentityStore,
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
await ensureIdentityTables(conn);
|
|
217
|
+
const exec = identitySqlExec(conn);
|
|
218
|
+
await hydrateIdentityStore(exec, store);
|
|
219
|
+
if (!store.persistUser || !store.persistAccount) {
|
|
220
|
+
bindIdentitySqlPersist(store, exec);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Bind host identity persist through {@link HostIdentitySqlRuntime.primarySql}.
|
|
226
|
+
*
|
|
227
|
+
* No-ops when the store runtime has no SQL driver (in-memory-only apps).
|
|
228
|
+
*
|
|
229
|
+
* @param runtime - Booted `store` element
|
|
230
|
+
* @param store - `gate.auth.identities`
|
|
231
|
+
*/
|
|
232
|
+
export async function bindHostIdentitySqlFromStore(
|
|
233
|
+
runtime: HostIdentitySqlRuntime,
|
|
234
|
+
store: IdentityStore,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const conn = await runtime.primarySql();
|
|
237
|
+
if (!conn) return;
|
|
238
|
+
await bindHostIdentitySql(conn, store);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function userFromSql(raw: Record<string, unknown>): UserIdentityRow | null {
|
|
242
|
+
const id = asString(raw.id ?? raw.ID);
|
|
243
|
+
const email = asString(raw.email);
|
|
244
|
+
if (!id || !email) return null;
|
|
245
|
+
return {
|
|
246
|
+
id,
|
|
247
|
+
email,
|
|
248
|
+
name: asString(raw.name) ?? "",
|
|
249
|
+
emailVerified: asBool(raw.email_verified ?? raw.emailVerified),
|
|
250
|
+
status: raw.status === "suspended" ? "suspended" : "active",
|
|
251
|
+
createdAt: asNumber(raw.created_at ?? raw.createdAt),
|
|
252
|
+
updatedAt: asNumber(raw.updated_at ?? raw.updatedAt),
|
|
253
|
+
extra: asExtra(raw.extra_json ?? raw.extra),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function accountFromSql(raw: Record<string, unknown>): UserAccountRow | null {
|
|
258
|
+
const id = asString(raw.id ?? raw.ID);
|
|
259
|
+
const userId = asString(raw.user_id ?? raw.userId);
|
|
260
|
+
const provider = asString(raw.provider);
|
|
261
|
+
const providerAccountId = asString(raw.provider_account_id ?? raw.providerAccountId);
|
|
262
|
+
if (!id || !userId || !provider || !providerAccountId) return null;
|
|
263
|
+
return {
|
|
264
|
+
id,
|
|
265
|
+
userId,
|
|
266
|
+
provider,
|
|
267
|
+
providerAccountId,
|
|
268
|
+
passwordHash: asNullableString(raw.password_hash ?? raw.passwordHash),
|
|
269
|
+
createdAt: asNumber(raw.created_at ?? raw.createdAt),
|
|
270
|
+
updatedAt: asNumber(raw.updated_at ?? raw.updatedAt),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeKey(email: string): string {
|
|
275
|
+
return email.trim().toLowerCase();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function asString(value: unknown): string | null {
|
|
279
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function asNullableString(value: unknown): string | null {
|
|
283
|
+
return typeof value === "string" ? value : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function asNumber(value: unknown): number {
|
|
287
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
288
|
+
if (typeof value === "string" && value.length > 0) {
|
|
289
|
+
const n = Number(value);
|
|
290
|
+
return Number.isFinite(n) ? n : 0;
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function asBool(value: unknown): boolean {
|
|
296
|
+
return value === 1 || value === true || value === "1" || value === "true";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function asExtra(value: unknown): Record<string, unknown> {
|
|
300
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
301
|
+
return value as Record<string, unknown>;
|
|
302
|
+
}
|
|
303
|
+
if (typeof value === "string" && value.length > 0) {
|
|
304
|
+
try {
|
|
305
|
+
const parsed: unknown = JSON.parse(value);
|
|
306
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
307
|
+
return parsed as Record<string, unknown>;
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {};
|
|
314
|
+
}
|
package/src/auth/identity.ts
CHANGED
|
@@ -39,6 +39,10 @@ export interface IdentityStore {
|
|
|
39
39
|
accounts: Map<string, UserAccountRow>;
|
|
40
40
|
/** `provider:providerAccountId` → account id. */
|
|
41
41
|
byProvider: Map<string, string>;
|
|
42
|
+
/** Optional write-through user persist (SQL). Bound at host boot. */
|
|
43
|
+
persistUser?: (row: UserIdentityRow) => void | Promise<void>;
|
|
44
|
+
/** Optional write-through account persist (SQL). Bound at host boot. */
|
|
45
|
+
persistAccount?: (row: UserAccountRow) => void | Promise<void>;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
@@ -84,11 +88,11 @@ export function normalizeEmail(email: string): string {
|
|
|
84
88
|
* @param email - Normalized email
|
|
85
89
|
* @param now - Clock
|
|
86
90
|
*/
|
|
87
|
-
export function ensureUserByEmail(
|
|
91
|
+
export async function ensureUserByEmail(
|
|
88
92
|
store: IdentityStore,
|
|
89
93
|
email: string,
|
|
90
94
|
now: number,
|
|
91
|
-
): UserIdentityRow {
|
|
95
|
+
): Promise<UserIdentityRow> {
|
|
92
96
|
const existingId = store.byEmail.get(email);
|
|
93
97
|
if (existingId) {
|
|
94
98
|
const existing = store.users.get(existingId);
|
|
@@ -107,6 +111,7 @@ export function ensureUserByEmail(
|
|
|
107
111
|
};
|
|
108
112
|
store.users.set(id, user);
|
|
109
113
|
store.byEmail.set(email, id);
|
|
114
|
+
await store.persistUser?.(user);
|
|
110
115
|
return user;
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -164,6 +169,7 @@ export async function createUserWithPassword(
|
|
|
164
169
|
store.byEmail.set(email, id);
|
|
165
170
|
store.accounts.set(accountId, account);
|
|
166
171
|
store.byProvider.set(`credential:${email}`, accountId);
|
|
172
|
+
await Promise.all([store.persistUser?.(user), store.persistAccount?.(account)]);
|
|
167
173
|
return user;
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -201,6 +207,138 @@ export function getUserById(store: IdentityStore, id: string): UserIdentityRow |
|
|
|
201
207
|
return store.users.get(id);
|
|
202
208
|
}
|
|
203
209
|
|
|
210
|
+
/** Options for {@link linkOrProvision}. */
|
|
211
|
+
export interface LinkOrProvisionOptions {
|
|
212
|
+
/** Provider name (`magic-link`, `otp`, `passkey`, `username`, `anonymous`). */
|
|
213
|
+
readonly provider: string;
|
|
214
|
+
/** Provider-scoped subject (email, phone, username, credential id). */
|
|
215
|
+
readonly providerAccountId: string;
|
|
216
|
+
/** Email to attach to a freshly provisioned user (normalized by the store). */
|
|
217
|
+
readonly email?: string;
|
|
218
|
+
/** Email-verified claim (only when provisioning). */
|
|
219
|
+
readonly emailVerified?: boolean;
|
|
220
|
+
/** Display name when provisioning. */
|
|
221
|
+
readonly name?: string;
|
|
222
|
+
/** Optional password hash for the account row. */
|
|
223
|
+
readonly passwordHash?: string | null;
|
|
224
|
+
/** Currently authenticated `fx.auth.userId`, when the request is authenticated. */
|
|
225
|
+
readonly currentUserId?: string;
|
|
226
|
+
/** Injectable clock. */
|
|
227
|
+
readonly now?: () => number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Result of {@link linkOrProvision}. */
|
|
231
|
+
export interface LinkedCredential {
|
|
232
|
+
readonly user: UserIdentityRow;
|
|
233
|
+
readonly account: UserAccountRow;
|
|
234
|
+
/** `true` when a new account was written (vs plain sign-in resolution). */
|
|
235
|
+
readonly created: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The single shared credential-write path across Gate auth method plugins.
|
|
240
|
+
*
|
|
241
|
+
* Enforces the locked account-linking rule centrally:
|
|
242
|
+
* - An existing `provider:providerAccountId` resolves to its user (sign-in).
|
|
243
|
+
* - A new credential may attach to an existing user ONLY when the request is
|
|
244
|
+
* already authenticated as that user (`currentUserId` matches).
|
|
245
|
+
* - An unauthenticated new credential whose email is already owned by another
|
|
246
|
+
* user is REFUSED (`email_in_use`) — never auto-linked by email match alone.
|
|
247
|
+
*
|
|
248
|
+
* @param store - Shared identity store
|
|
249
|
+
* @param options - Credential + linking context
|
|
250
|
+
*/
|
|
251
|
+
export async function linkOrProvision(
|
|
252
|
+
store: IdentityStore,
|
|
253
|
+
options: LinkOrProvisionOptions,
|
|
254
|
+
): Promise<LinkedCredential> {
|
|
255
|
+
const { provider, providerAccountId } = options;
|
|
256
|
+
const now = options.now ?? (() => Date.now());
|
|
257
|
+
const key = `${provider}:${providerAccountId}`;
|
|
258
|
+
|
|
259
|
+
// 1. Existing credential → plain sign-in resolution (no linking needed).
|
|
260
|
+
const existingAccountId = store.byProvider.get(key);
|
|
261
|
+
if (existingAccountId) {
|
|
262
|
+
const account = store.accounts.get(existingAccountId);
|
|
263
|
+
const user = account ? store.users.get(account.userId) : undefined;
|
|
264
|
+
if (account && user) return { user, account, created: false };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const email = options.email ? normalizeEmail(options.email) : undefined;
|
|
268
|
+
const emailOwnerId = email ? store.byEmail.get(email) : undefined;
|
|
269
|
+
if (emailOwnerId) {
|
|
270
|
+
if (options.currentUserId) {
|
|
271
|
+
if (emailOwnerId !== options.currentUserId) {
|
|
272
|
+
throw new IdentityError("email_conflict", "email belongs to another user");
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
throw new IdentityError("email_in_use", "email already registered to another credential");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const t = now();
|
|
280
|
+
const userId = options.currentUserId ?? crypto.randomUUID();
|
|
281
|
+
const existingUser = store.users.get(userId);
|
|
282
|
+
const user: UserIdentityRow = existingUser ?? {
|
|
283
|
+
id: userId,
|
|
284
|
+
email: email ?? "",
|
|
285
|
+
name: options.name?.trim() || (email ? email.split("@")[0] || "user" : "user"),
|
|
286
|
+
emailVerified: options.emailVerified === true,
|
|
287
|
+
status: "active",
|
|
288
|
+
createdAt: t,
|
|
289
|
+
updatedAt: t,
|
|
290
|
+
extra: {},
|
|
291
|
+
};
|
|
292
|
+
const account: UserAccountRow = {
|
|
293
|
+
id: crypto.randomUUID(),
|
|
294
|
+
userId,
|
|
295
|
+
provider,
|
|
296
|
+
providerAccountId,
|
|
297
|
+
passwordHash: options.passwordHash ?? null,
|
|
298
|
+
createdAt: t,
|
|
299
|
+
updatedAt: t,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const userIsNew = !store.users.has(userId);
|
|
303
|
+
store.users.set(userId, user);
|
|
304
|
+
if (email) store.byEmail.set(email, userId);
|
|
305
|
+
store.accounts.set(account.id, account);
|
|
306
|
+
store.byProvider.set(key, account.id);
|
|
307
|
+
if (userIsNew) await store.persistUser?.(user);
|
|
308
|
+
await store.persistAccount?.(account);
|
|
309
|
+
return { user, account, created: true };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Ensure a user row exists for `userId` (defensive — two-factor enable on an
|
|
314
|
+
* established session principal).
|
|
315
|
+
*
|
|
316
|
+
* @param store - Shared identity store
|
|
317
|
+
* @param userId - Existing principal id
|
|
318
|
+
* @param now - Clock
|
|
319
|
+
*/
|
|
320
|
+
export async function ensureUserExists(
|
|
321
|
+
store: IdentityStore,
|
|
322
|
+
userId: string,
|
|
323
|
+
now: number,
|
|
324
|
+
): Promise<UserIdentityRow> {
|
|
325
|
+
const existing = store.users.get(userId);
|
|
326
|
+
if (existing) return existing;
|
|
327
|
+
const user: UserIdentityRow = {
|
|
328
|
+
id: userId,
|
|
329
|
+
email: "",
|
|
330
|
+
name: "user",
|
|
331
|
+
emailVerified: false,
|
|
332
|
+
status: "active",
|
|
333
|
+
createdAt: now,
|
|
334
|
+
updatedAt: now,
|
|
335
|
+
extra: {},
|
|
336
|
+
};
|
|
337
|
+
store.users.set(userId, user);
|
|
338
|
+
await store.persistUser?.(user);
|
|
339
|
+
return user;
|
|
340
|
+
}
|
|
341
|
+
|
|
204
342
|
/** Identity-plane error. */
|
|
205
343
|
export class IdentityError extends Error {
|
|
206
344
|
readonly code: string;
|
package/src/auth/index.ts
CHANGED
|
@@ -88,11 +88,15 @@ export {
|
|
|
88
88
|
getUserById,
|
|
89
89
|
normalizeEmail,
|
|
90
90
|
ensureUserByEmail,
|
|
91
|
+
ensureUserExists,
|
|
92
|
+
linkOrProvision,
|
|
91
93
|
IdentityError,
|
|
92
94
|
type IdentityStore,
|
|
93
95
|
type UserIdentityRow,
|
|
94
96
|
type UserAccountRow,
|
|
95
97
|
type CreateUserWithPasswordOptions,
|
|
98
|
+
type LinkOrProvisionOptions,
|
|
99
|
+
type LinkedCredential,
|
|
96
100
|
} from "./identity.ts";
|
|
97
101
|
|
|
98
102
|
export {
|
|
@@ -196,6 +200,19 @@ export {
|
|
|
196
200
|
type HostApiKeySqlRuntime,
|
|
197
201
|
} from "./api-key-sql.ts";
|
|
198
202
|
|
|
203
|
+
export {
|
|
204
|
+
identitySqlExec,
|
|
205
|
+
bindIdentitySqlPersist,
|
|
206
|
+
bindHostIdentitySql,
|
|
207
|
+
bindHostIdentitySqlFromStore,
|
|
208
|
+
ensureIdentityTables,
|
|
209
|
+
hydrateIdentityStore,
|
|
210
|
+
persistIdentityUserRow,
|
|
211
|
+
persistIdentityAccountRow,
|
|
212
|
+
type IdentitySqlExec,
|
|
213
|
+
type HostIdentitySqlRuntime,
|
|
214
|
+
} from "./identity-sql.ts";
|
|
215
|
+
|
|
199
216
|
export {
|
|
200
217
|
createOperatorInviteStore,
|
|
201
218
|
createOperatorInvite,
|
|
@@ -250,7 +267,6 @@ export { invokeAs, type InvokeAsOptions } from "./invoke-as.ts";
|
|
|
250
267
|
export {
|
|
251
268
|
AUTH_TABLES,
|
|
252
269
|
type ApiKeyRow,
|
|
253
|
-
type IdentityRow,
|
|
254
270
|
type OperatorCredentialRow,
|
|
255
271
|
type OperatorInviteRow,
|
|
256
272
|
type OperatorRow,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
10
10
|
import type { BreachCheckFn } from "./breach-check.ts";
|
|
11
|
+
import type { IdentityStore } from "./identity.ts";
|
|
11
12
|
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
12
13
|
import type { SessionStore } from "./sessions.ts";
|
|
13
14
|
|
|
@@ -15,6 +16,8 @@ import type { SessionStore } from "./sessions.ts";
|
|
|
15
16
|
export interface ActiveGateAuthContext {
|
|
16
17
|
readonly secret: string;
|
|
17
18
|
readonly sessions: SessionStore;
|
|
19
|
+
/** Shared identity/credential store — method plugins resolve users here. */
|
|
20
|
+
readonly identities?: IdentityStore;
|
|
18
21
|
readonly now?: () => number;
|
|
19
22
|
/** From `gate.auth.passwordPolicy` — shared by credential method plugins. */
|
|
20
23
|
readonly passwordPolicy?: PasswordPolicyOptions;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIMD (draft-ietf-oauth-client-id-metadata-document) client resolution —
|
|
3
|
+
* fetch the metadata document AT the `client_id` URL, validate it, and cache.
|
|
4
|
+
*
|
|
5
|
+
* This is a cache, not a DCR registry (locked decision 1): clients register
|
|
6
|
+
* nowhere; the document at their own HTTPS origin is the registration. The
|
|
7
|
+
* `client_id` host must differ from the AS issuer host so a client cannot
|
|
8
|
+
* self-host its metadata on the authorization server.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { OAuthError } from "./errors.ts";
|
|
12
|
+
import type { ClientCacheRow, CimdClientMetadata } from "./tables.ts";
|
|
13
|
+
|
|
14
|
+
/** Cache lifetime for fetched CIMD documents. */
|
|
15
|
+
export const CIMD_CACHE_TTL_MS = 5 * 60_000;
|
|
16
|
+
|
|
17
|
+
const REQUIRED_REDIRECT_SCHEMES = new Set(["https", "http"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate a raw CIMD document against the URL it was fetched from.
|
|
21
|
+
*
|
|
22
|
+
* @param url - The `client_id` URL the document was served from
|
|
23
|
+
* @param raw - Parsed JSON body
|
|
24
|
+
*/
|
|
25
|
+
export function parseCimdMetadata(url: string, raw: unknown): CimdClientMetadata {
|
|
26
|
+
if (typeof raw !== "object" || raw === null) {
|
|
27
|
+
throw new OAuthError("invalid_request", "client metadata document is not an object");
|
|
28
|
+
}
|
|
29
|
+
const doc = raw as Record<string, unknown>;
|
|
30
|
+
const clientId = doc.client_id;
|
|
31
|
+
if (typeof clientId !== "string" || clientId !== url) {
|
|
32
|
+
throw new OAuthError("invalid_request", "metadata client_id must equal the document URL");
|
|
33
|
+
}
|
|
34
|
+
const redirectUris = doc.redirect_uris;
|
|
35
|
+
if (
|
|
36
|
+
!Array.isArray(redirectUris) ||
|
|
37
|
+
redirectUris.length === 0 ||
|
|
38
|
+
!redirectUris.every((u) => typeof u === "string" && isAcceptableRedirect(u))
|
|
39
|
+
) {
|
|
40
|
+
throw new OAuthError(
|
|
41
|
+
"invalid_request",
|
|
42
|
+
"metadata redirect_uris must be absolute https/http URIs",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
client_id: clientId,
|
|
47
|
+
...(typeof doc.client_name === "string" ? { client_name: doc.client_name } : {}),
|
|
48
|
+
redirect_uris: redirectUris as readonly string[],
|
|
49
|
+
...(Array.isArray(doc.grant_types)
|
|
50
|
+
? {
|
|
51
|
+
grant_types: (doc.grant_types as unknown[]).filter(
|
|
52
|
+
(g): g is string => typeof g === "string",
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
56
|
+
...(Array.isArray(doc.response_types)
|
|
57
|
+
? {
|
|
58
|
+
response_types: (doc.response_types as unknown[]).filter(
|
|
59
|
+
(r): r is string => typeof r === "string",
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
63
|
+
...(typeof doc.token_endpoint_auth_method === "string"
|
|
64
|
+
? { token_endpoint_auth_method: doc.token_endpoint_auth_method }
|
|
65
|
+
: {}),
|
|
66
|
+
...(typeof doc.scope === "string" ? { scope: doc.scope } : {}),
|
|
67
|
+
...(typeof doc.jwks_uri === "string" ? { jwks_uri: doc.jwks_uri } : {}),
|
|
68
|
+
...(doc.jwks !== undefined ? { jwks: doc.jwks } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a `client_id` through the CIMD cache, refreshing when stale.
|
|
74
|
+
*
|
|
75
|
+
* @param cache - Cache map keyed by client id
|
|
76
|
+
* @param clientId - Must be an https URL
|
|
77
|
+
* @param now - Epoch-ms
|
|
78
|
+
* @param fetchDoc - Document loader (injectable for tests)
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveCimdClient(
|
|
81
|
+
cache: Map<string, ClientCacheRow>,
|
|
82
|
+
clientId: string,
|
|
83
|
+
now: number,
|
|
84
|
+
fetchDoc: (url: string) => Promise<unknown> = defaultFetch,
|
|
85
|
+
): Promise<ClientCacheRow> {
|
|
86
|
+
let parsed: URL;
|
|
87
|
+
try {
|
|
88
|
+
parsed = new URL(clientId);
|
|
89
|
+
} catch {
|
|
90
|
+
throw new OAuthError("invalid_request", "client_id must be an https URL");
|
|
91
|
+
}
|
|
92
|
+
if (parsed.protocol !== "https:") {
|
|
93
|
+
throw new OAuthError("invalid_request", "client_id must be an https URL");
|
|
94
|
+
}
|
|
95
|
+
const cached = cache.get(clientId);
|
|
96
|
+
if (cached && cached.deniedAt === null && cached.fetchedAt + CIMD_CACHE_TTL_MS > now) {
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
let raw: unknown;
|
|
100
|
+
try {
|
|
101
|
+
raw = await fetchDoc(clientId);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new OAuthError("invalid_request", "failed to fetch client metadata document");
|
|
104
|
+
}
|
|
105
|
+
const metadata = parseCimdMetadata(clientId, raw);
|
|
106
|
+
const row: ClientCacheRow = { clientId, metadata, fetchedAt: now, deniedAt: null };
|
|
107
|
+
cache.set(clientId, row);
|
|
108
|
+
return row;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isAcceptableRedirect(uri: string): boolean {
|
|
112
|
+
let parsed: URL;
|
|
113
|
+
try {
|
|
114
|
+
parsed = new URL(uri);
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (!REQUIRED_REDIRECT_SCHEMES.has(parsed.protocol.replace(":", ""))) return false;
|
|
119
|
+
// http is only acceptable on loopback (native/dev clients).
|
|
120
|
+
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) return false;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isLoopbackHost(host: string): boolean {
|
|
125
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function defaultFetch(url: string): Promise<unknown> {
|
|
129
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
130
|
+
if (!response.ok) throw new Error(`cimd fetch failed: ${response.status}`);
|
|
131
|
+
return (await response.json()) as unknown;
|
|
132
|
+
}
|