@byollm/conformance 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/chunk-PDQJJ3Q2.js +880 -0
- package/dist/chunk-PDQJJ3Q2.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
- package/targets/supabase.ts +211 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Certify the Supabase adapter with the conformance kit.
|
|
3
|
+
*
|
|
4
|
+
* Run against a local stack:
|
|
5
|
+
*
|
|
6
|
+
* ```bash
|
|
7
|
+
* cd packages/server && supabase start
|
|
8
|
+
* pnpm --filter @byollm/server run conformance:supabase
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* "A server is byollm-compatible when the kit passes" — this script is that
|
|
12
|
+
* sentence applied to the first-party adapter. It is the same kit, the same
|
|
13
|
+
* checks and the same real daemon that certify the in-memory reference, so a
|
|
14
|
+
* behaviour the two stores disagree about fails here rather than in someone's
|
|
15
|
+
* production queue.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { createClient } from "@supabase/supabase-js";
|
|
19
|
+
import {
|
|
20
|
+
certify,
|
|
21
|
+
formatReport,
|
|
22
|
+
type ConformanceTarget,
|
|
23
|
+
} from "@byollm/conformance";
|
|
24
|
+
// The built package, not `../src`: Node's type stripping does not rewrite the
|
|
25
|
+
// `.js` specifiers the source uses, and certifying the published entry points
|
|
26
|
+
// is closer to what a consumer actually gets.
|
|
27
|
+
import { ByollmApp, createFetchHandler } from "@byollm/server";
|
|
28
|
+
import { supabaseStore } from "@byollm/server/supabase";
|
|
29
|
+
|
|
30
|
+
const SUPABASE_URL = process.env["SUPABASE_URL"] ?? "http://127.0.0.1:54421";
|
|
31
|
+
const SERVICE_KEY =
|
|
32
|
+
process.env["SUPABASE_SERVICE_ROLE_KEY"] ??
|
|
33
|
+
process.env["SUPABASE_SECRET_KEY"] ??
|
|
34
|
+
"";
|
|
35
|
+
|
|
36
|
+
const ORIGIN = "https://supabase.byollm.test";
|
|
37
|
+
/** Short, because a real Postgres clock cannot be faked forward. */
|
|
38
|
+
const LEASE_MS = 2_000;
|
|
39
|
+
const TTL_MS = 1_500;
|
|
40
|
+
|
|
41
|
+
if (SERVICE_KEY === "") {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
"SUPABASE_SERVICE_ROLE_KEY (or SUPABASE_SECRET_KEY) is required.\n" +
|
|
44
|
+
"Run `supabase status` in packages/server to find it.\n",
|
|
45
|
+
);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// `createClient` infers a wider generic than the bare `SupabaseClient` alias;
|
|
50
|
+
// letting it infer avoids an assignment the linter cannot verify.
|
|
51
|
+
const client = createClient(SUPABASE_URL, SERVICE_KEY, {
|
|
52
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The kit talks about owners as names ("alice"); Postgres needs uuids that
|
|
57
|
+
* reference `auth.users`. This maps between them so the checks read the same
|
|
58
|
+
* against both stores.
|
|
59
|
+
*/
|
|
60
|
+
const owners = new Map<string, string>();
|
|
61
|
+
const ownerNames = new Map<string, string>();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Per-run email suffix.
|
|
65
|
+
*
|
|
66
|
+
* Users are created fresh each run rather than looked up, because
|
|
67
|
+
* `auth.admin.listUsers` is unreliable on the local stack ("Database error
|
|
68
|
+
* finding users") and a certification run must not depend on it. Unique
|
|
69
|
+
* emails mean `createUser` always succeeds and never needs a fallback.
|
|
70
|
+
*/
|
|
71
|
+
const RUN = randomUUID().slice(0, 8);
|
|
72
|
+
|
|
73
|
+
async function ensureUser(name: string): Promise<string> {
|
|
74
|
+
const existing = owners.get(name);
|
|
75
|
+
if (existing !== undefined) return existing;
|
|
76
|
+
|
|
77
|
+
const { data, error } = await client.auth.admin.createUser({
|
|
78
|
+
email: `${name}+${RUN}@byollm.test`,
|
|
79
|
+
email_confirm: true,
|
|
80
|
+
});
|
|
81
|
+
if (error)
|
|
82
|
+
throw new Error(`could not create test user ${name}: ${error.message}`);
|
|
83
|
+
|
|
84
|
+
const id = data.user.id;
|
|
85
|
+
owners.set(name, id);
|
|
86
|
+
ownerNames.set(id, name);
|
|
87
|
+
return id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const toName = (id: string): string => ownerNames.get(id) ?? id;
|
|
91
|
+
|
|
92
|
+
const store = supabaseStore({ client, defaultTtlMs: TTL_MS });
|
|
93
|
+
const app = new ByollmApp({ store });
|
|
94
|
+
const handler = createFetchHandler({
|
|
95
|
+
store,
|
|
96
|
+
verificationUrl: `${ORIGIN}/settings/runners`,
|
|
97
|
+
leaseMs: LEASE_MS,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const target: ConformanceTarget = {
|
|
101
|
+
name: "@byollm/server (Supabase adapter)",
|
|
102
|
+
origin: ORIGIN,
|
|
103
|
+
leaseMs: LEASE_MS,
|
|
104
|
+
ttlMs: TTL_MS,
|
|
105
|
+
// No `advanceTime`: a real Postgres clock cannot be moved, so the kit waits
|
|
106
|
+
// for real. That is why the lease and TTL above are short.
|
|
107
|
+
|
|
108
|
+
fetch: (request) => handler(request),
|
|
109
|
+
|
|
110
|
+
enqueue: async (input) => {
|
|
111
|
+
const handle = await app.enqueue({
|
|
112
|
+
kind: input.kind,
|
|
113
|
+
payload: input.payload,
|
|
114
|
+
owner: await ensureUser(input.owner),
|
|
115
|
+
...(input.audience === undefined ? {} : { audience: input.audience }),
|
|
116
|
+
...(input.audienceAllow === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: {
|
|
119
|
+
audienceAllow: await Promise.all(
|
|
120
|
+
input.audienceAllow.map((name) => ensureUser(name)),
|
|
121
|
+
),
|
|
122
|
+
}),
|
|
123
|
+
...(input.dependsOn === undefined ? {} : { dependsOn: input.dependsOn }),
|
|
124
|
+
...(input.ttlMs === undefined ? {} : { ttlMs: input.ttlMs }),
|
|
125
|
+
});
|
|
126
|
+
return { id: handle.id };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
approvePairing: async (userCode, owner) => {
|
|
130
|
+
await app.approvePairing({ userCode, owner: await ensureUser(owner) });
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
revokeRunner: (runnerId) => app.revokeRunner(runnerId),
|
|
134
|
+
|
|
135
|
+
cancelJob: async (jobId) => {
|
|
136
|
+
await app.cancel(jobId);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
job: async (jobId) => {
|
|
140
|
+
const record = await app.job(jobId);
|
|
141
|
+
if (!record) return null;
|
|
142
|
+
return {
|
|
143
|
+
state: record.state,
|
|
144
|
+
...(record.outcome === null
|
|
145
|
+
? {}
|
|
146
|
+
: {
|
|
147
|
+
outcome: {
|
|
148
|
+
outcome: record.outcome.outcome,
|
|
149
|
+
...(record.outcome.outcome === "ok"
|
|
150
|
+
? { text: record.outcome.text }
|
|
151
|
+
: {}),
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
...(record.provenance === null
|
|
155
|
+
? {}
|
|
156
|
+
: {
|
|
157
|
+
provenance: {
|
|
158
|
+
untrusted: record.provenance.untrusted,
|
|
159
|
+
audience: record.provenance.audience,
|
|
160
|
+
runnerOwner: toName(record.provenance.runnerOwner),
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
runnerAvailability: async (input) => {
|
|
167
|
+
const availability = await app.runnerAvailability({
|
|
168
|
+
kind: input.kind,
|
|
169
|
+
owner: await ensureUser(input.owner),
|
|
170
|
+
...(input.audience === undefined ? {} : { audience: input.audience }),
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
available: availability.available,
|
|
174
|
+
...(availability.reason === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: { reason: availability.reason }),
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
sweep: async () => {
|
|
181
|
+
await app.sweep();
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Owner ids here are `auth.users` uuids, not the names the checks use.
|
|
185
|
+
ownerId: (name) => ensureUser(name),
|
|
186
|
+
|
|
187
|
+
reset: async () => {
|
|
188
|
+
// Truncate rather than drop: the migration is what is under test, and
|
|
189
|
+
// re-running it between checks would be testing the migration instead.
|
|
190
|
+
// Each table names its own primary key — `byollm_job_cancels` is keyed by
|
|
191
|
+
// `job_id`, and PostgREST requires a filter on every delete.
|
|
192
|
+
const tables: readonly [string, string][] = [
|
|
193
|
+
["byollm_job_cancels", "job_id"],
|
|
194
|
+
["byollm_jobs", "id"],
|
|
195
|
+
["byollm_pairings", "device_code_hash"],
|
|
196
|
+
["byollm_runners", "id"],
|
|
197
|
+
];
|
|
198
|
+
for (const [table, key] of tables) {
|
|
199
|
+
const { error } = await client.from(table).delete().not(key, "is", null);
|
|
200
|
+
if (error) throw new Error(`reset ${table}: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const report = await certify(target, {
|
|
206
|
+
onProgress: (result) => {
|
|
207
|
+
process.stdout.write(result.passed ? "." : "x");
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
process.stdout.write(`\n\n${formatReport(report)}`);
|
|
211
|
+
process.exit(report.passed ? 0 : 1);
|