@hatk/hatk 0.0.1-alpha.8 → 0.0.1-alpha.81
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/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +111 -0
- package/dist/backfill.d.ts +29 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +136 -51
- package/dist/car.d.ts +42 -10
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +154 -14
- package/dist/cli.js +243 -1043
- package/dist/config.d.ts +82 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +44 -9
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +16 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +76 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/{db.d.ts → database/db.d.ts} +57 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/{db.js → database/db.js} +755 -552
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +116 -32
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/{schema.d.ts → database/schema.d.ts} +14 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +81 -41
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +125 -0
- package/dist/feeds.d.ts +12 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +51 -6
- package/dist/hooks.d.ts +85 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +163 -0
- package/dist/hydrate.d.ts +7 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +4 -16
- package/dist/indexer.d.ts +133 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +406 -66
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +225 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +169 -74
- package/dist/mst.d.ts +18 -1
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +19 -8
- package/dist/oauth/db.d.ts +3 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +56 -19
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +13 -1
- package/dist/oauth/server.d.ts +92 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +352 -37
- package/dist/oauth/service-describe.d.ts +31 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +61 -0
- package/dist/oauth/session.d.ts +32 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +131 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +80 -40
- package/dist/pds-proxy.d.ts +82 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +356 -0
- package/dist/private-collections.d.ts +8 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +22 -0
- package/dist/push.d.ts +54 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +367 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +88 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +62 -0
- package/dist/server.d.ts +26 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +858 -638
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/templates/feed.tpl +14 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +41 -0
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +41 -32
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +84 -11
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +211 -41
- package/package.json +17 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
package/dist/push.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push notification delivery via APNs HTTP/2 and FCM HTTP v1.
|
|
3
|
+
*
|
|
4
|
+
* Provides `push.send()` for use in on-commit hook context. Looks up device
|
|
5
|
+
* tokens, builds a payload per platform, and sends over the transport that
|
|
6
|
+
* platform's tokens belong to. Self-cleans tokens the vendor reports as dead —
|
|
7
|
+
* Apple's 410, Google's UNREGISTERED. Fire-and-forget — failures are logged via
|
|
8
|
+
* `emit()` but never throw.
|
|
9
|
+
*
|
|
10
|
+
* Either transport can be configured alone. A deployment with only an APNs key
|
|
11
|
+
* still sends to iOS, and Android tokens simply sit unused until an FCM service
|
|
12
|
+
* account is supplied.
|
|
13
|
+
*/
|
|
14
|
+
import { connect } from 'node:http2';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { createSign } from 'node:crypto';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
import { emit } from "./logger.js";
|
|
19
|
+
import { runSQL, querySQL } from "./database/db.js";
|
|
20
|
+
let pushConfig = null;
|
|
21
|
+
let apnsKey = null;
|
|
22
|
+
let cachedJwt = null;
|
|
23
|
+
let http2Session = null;
|
|
24
|
+
let fcmCredentials = null;
|
|
25
|
+
let cachedFcmToken = null;
|
|
26
|
+
/** Where a service account trades its signed JWT for a bearer token. */
|
|
27
|
+
const GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token';
|
|
28
|
+
/** The only scope FCM sending needs. */
|
|
29
|
+
const FCM_SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';
|
|
30
|
+
/**
|
|
31
|
+
* Initialize push with config. Must be called before send().
|
|
32
|
+
*
|
|
33
|
+
* Each transport is loaded on its own: a missing or malformed credential
|
|
34
|
+
* disables that platform and leaves the other one sending, rather than taking
|
|
35
|
+
* push down entirely.
|
|
36
|
+
*/
|
|
37
|
+
export function initPush(config, configDir) {
|
|
38
|
+
pushConfig = config;
|
|
39
|
+
apnsKey = null;
|
|
40
|
+
fcmCredentials = null;
|
|
41
|
+
cachedJwt = null;
|
|
42
|
+
cachedFcmToken = null;
|
|
43
|
+
if (config.apns) {
|
|
44
|
+
const keyPath = resolve(configDir, config.apns.keyFile);
|
|
45
|
+
try {
|
|
46
|
+
apnsKey = readFileSync(keyPath, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
emit('push', 'init_error', { transport: 'apns', error: `APNs key file not found: ${keyPath}` });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (config.fcm) {
|
|
53
|
+
const keyPath = resolve(configDir, config.fcm.keyFile);
|
|
54
|
+
try {
|
|
55
|
+
const account = JSON.parse(readFileSync(keyPath, 'utf8'));
|
|
56
|
+
const projectId = config.fcm.projectId ?? account.project_id;
|
|
57
|
+
if (!account.client_email || !account.private_key || !projectId) {
|
|
58
|
+
throw new Error('needs client_email, private_key and a project id');
|
|
59
|
+
}
|
|
60
|
+
fcmCredentials = {
|
|
61
|
+
clientEmail: account.client_email,
|
|
62
|
+
privateKey: account.private_key,
|
|
63
|
+
projectId,
|
|
64
|
+
tokenUri: account.token_uri ?? GOOGLE_TOKEN_URI,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
emit('push', 'init_error', {
|
|
69
|
+
transport: 'fcm',
|
|
70
|
+
error: `FCM service account unusable (${keyPath}): ${err.message}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!apnsKey && !fcmCredentials)
|
|
75
|
+
pushConfig = null;
|
|
76
|
+
}
|
|
77
|
+
/** Check if push is configured and at least one transport is usable. */
|
|
78
|
+
export function isPushEnabled() {
|
|
79
|
+
return pushConfig !== null && (apnsKey !== null || fcmCredentials !== null);
|
|
80
|
+
}
|
|
81
|
+
/** Which platforms this process can actually deliver to, for startup logging. */
|
|
82
|
+
export function enabledPushTransports() {
|
|
83
|
+
const transports = [];
|
|
84
|
+
if (apnsKey)
|
|
85
|
+
transports.push('apns');
|
|
86
|
+
if (fcmCredentials)
|
|
87
|
+
transports.push('fcm');
|
|
88
|
+
return transports;
|
|
89
|
+
}
|
|
90
|
+
/** Build the push interface injected into hook contexts. */
|
|
91
|
+
export function buildPushInterface() {
|
|
92
|
+
return { send };
|
|
93
|
+
}
|
|
94
|
+
/** Create a JWT for APNs authentication (cached for 50 minutes). */
|
|
95
|
+
function getApnsJwt() {
|
|
96
|
+
if (cachedJwt && Date.now() < cachedJwt.expires)
|
|
97
|
+
return cachedJwt.token;
|
|
98
|
+
const apns = pushConfig?.apns;
|
|
99
|
+
if (!apns || !apnsKey)
|
|
100
|
+
throw new Error('APNs not initialized');
|
|
101
|
+
const header = Buffer.from(JSON.stringify({
|
|
102
|
+
alg: 'ES256',
|
|
103
|
+
kid: apns.keyId,
|
|
104
|
+
})).toString('base64url');
|
|
105
|
+
const now = Math.floor(Date.now() / 1000);
|
|
106
|
+
const claims = Buffer.from(JSON.stringify({
|
|
107
|
+
iss: apns.teamId,
|
|
108
|
+
iat: now,
|
|
109
|
+
})).toString('base64url');
|
|
110
|
+
const signer = createSign('SHA256');
|
|
111
|
+
signer.update(`${header}.${claims}`);
|
|
112
|
+
const signature = signer.sign(apnsKey, 'base64url');
|
|
113
|
+
const token = `${header}.${claims}.${signature}`;
|
|
114
|
+
cachedJwt = { token, expires: Date.now() + 50 * 60 * 1000 };
|
|
115
|
+
return token;
|
|
116
|
+
}
|
|
117
|
+
/** Get or create an HTTP/2 connection to APNs. */
|
|
118
|
+
function getHttp2Session() {
|
|
119
|
+
if (http2Session && !http2Session.closed && !http2Session.destroyed) {
|
|
120
|
+
return http2Session;
|
|
121
|
+
}
|
|
122
|
+
const host = pushConfig?.apns?.production !== false ? 'https://api.push.apple.com' : 'https://api.sandbox.push.apple.com';
|
|
123
|
+
emit('push', 'connecting', { host });
|
|
124
|
+
http2Session = connect(host, {
|
|
125
|
+
peerMaxConcurrentStreams: 100,
|
|
126
|
+
});
|
|
127
|
+
http2Session.on('connect', () => {
|
|
128
|
+
emit('push', 'connected', { host });
|
|
129
|
+
});
|
|
130
|
+
http2Session.on('error', (err) => {
|
|
131
|
+
emit('push', 'connection_error', { host, error: err.message });
|
|
132
|
+
http2Session = null;
|
|
133
|
+
});
|
|
134
|
+
http2Session.on('close', () => {
|
|
135
|
+
http2Session = null;
|
|
136
|
+
});
|
|
137
|
+
return http2Session;
|
|
138
|
+
}
|
|
139
|
+
/** Send a push notification to all devices registered for a DID. */
|
|
140
|
+
async function send(payload) {
|
|
141
|
+
if (!isPushEnabled())
|
|
142
|
+
return;
|
|
143
|
+
const tokens = (await querySQL(`SELECT token, platform FROM _push_tokens WHERE did = $1`, [payload.did]));
|
|
144
|
+
if (tokens.length === 0)
|
|
145
|
+
return;
|
|
146
|
+
// A device is reachable over exactly one transport, named by the platform it
|
|
147
|
+
// registered under. An unrecognized platform is left alone rather than
|
|
148
|
+
// guessed at — the row may belong to a client this build doesn't serve yet.
|
|
149
|
+
const apnsTokens = tokens.filter((t) => t.platform === 'apns');
|
|
150
|
+
const fcmTokens = tokens.filter((t) => t.platform === 'fcm');
|
|
151
|
+
if (apnsKey && apnsTokens.length > 0) {
|
|
152
|
+
const jwt = getApnsJwt();
|
|
153
|
+
const aps = {
|
|
154
|
+
alert: { title: payload.title, body: payload.body },
|
|
155
|
+
sound: 'default',
|
|
156
|
+
};
|
|
157
|
+
if (payload.badge !== undefined) {
|
|
158
|
+
aps.badge = payload.badge;
|
|
159
|
+
}
|
|
160
|
+
const apnsPayload = JSON.stringify({
|
|
161
|
+
aps,
|
|
162
|
+
...(payload.data || {}),
|
|
163
|
+
});
|
|
164
|
+
for (const { token } of apnsTokens) {
|
|
165
|
+
sendToApns(token, apnsPayload, jwt, payload).catch(() => { });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (fcmCredentials && fcmTokens.length > 0) {
|
|
169
|
+
// One access token serves every device in this fan-out; it's minted once
|
|
170
|
+
// and cached for the hour Google grants it.
|
|
171
|
+
let accessToken;
|
|
172
|
+
try {
|
|
173
|
+
accessToken = await getFcmAccessToken();
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
emit('push', 'send_error', { did: payload.did, transport: 'fcm', error: err.message });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const { token } of fcmTokens) {
|
|
180
|
+
sendToFcm(token, accessToken, payload).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Send a single APNs push and handle the response. */
|
|
185
|
+
async function sendToApns(token, payload, jwt, original) {
|
|
186
|
+
const apns = pushConfig?.apns;
|
|
187
|
+
if (!apns)
|
|
188
|
+
return;
|
|
189
|
+
const session = getHttp2Session();
|
|
190
|
+
const headers = {
|
|
191
|
+
':method': 'POST',
|
|
192
|
+
':path': `/3/device/${token}`,
|
|
193
|
+
authorization: `bearer ${jwt}`,
|
|
194
|
+
'apns-topic': apns.bundleId,
|
|
195
|
+
'apns-push-type': 'alert',
|
|
196
|
+
};
|
|
197
|
+
if (original.collapseId) {
|
|
198
|
+
headers['apns-collapse-id'] = original.collapseId;
|
|
199
|
+
}
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const req = session.request(headers);
|
|
202
|
+
let settled = false;
|
|
203
|
+
const done = () => {
|
|
204
|
+
if (settled)
|
|
205
|
+
return;
|
|
206
|
+
settled = true;
|
|
207
|
+
resolve();
|
|
208
|
+
};
|
|
209
|
+
req.setTimeout(15_000, () => {
|
|
210
|
+
req.close();
|
|
211
|
+
emit('push', 'send_error', { did: original.did, error: 'APNs request timed out' });
|
|
212
|
+
done();
|
|
213
|
+
});
|
|
214
|
+
let status = 0;
|
|
215
|
+
let body = '';
|
|
216
|
+
req.on('response', (headers) => {
|
|
217
|
+
status = headers[':status'];
|
|
218
|
+
});
|
|
219
|
+
req.on('data', (chunk) => {
|
|
220
|
+
body += chunk.toString();
|
|
221
|
+
});
|
|
222
|
+
req.on('end', async () => {
|
|
223
|
+
if (settled)
|
|
224
|
+
return;
|
|
225
|
+
if (status === 200) {
|
|
226
|
+
emit('push', 'sent', { did: original.did, token: token.slice(0, 8) + '...' });
|
|
227
|
+
}
|
|
228
|
+
else if (status === 410) {
|
|
229
|
+
// Token is no longer valid — remove it
|
|
230
|
+
await removeToken(token).catch(() => { });
|
|
231
|
+
emit('push', 'token_removed', { did: original.did, reason: 'expired' });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
emit('push', 'send_error', {
|
|
235
|
+
did: original.did,
|
|
236
|
+
status,
|
|
237
|
+
body: body.slice(0, 200),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
done();
|
|
241
|
+
});
|
|
242
|
+
req.on('error', (err) => {
|
|
243
|
+
if (settled)
|
|
244
|
+
return;
|
|
245
|
+
emit('push', 'send_error', { did: original.did, error: err.message });
|
|
246
|
+
done();
|
|
247
|
+
});
|
|
248
|
+
req.write(payload);
|
|
249
|
+
req.end();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Mint an OAuth2 access token for the FCM HTTP v1 API.
|
|
254
|
+
*
|
|
255
|
+
* The service account signs a JWT asserting the messaging scope, and Google
|
|
256
|
+
* trades it for a bearer token good for an hour. Cached with a minute of
|
|
257
|
+
* headroom so one can't expire between minting and the send that uses it.
|
|
258
|
+
*/
|
|
259
|
+
async function getFcmAccessToken() {
|
|
260
|
+
if (cachedFcmToken && Date.now() < cachedFcmToken.expires)
|
|
261
|
+
return cachedFcmToken.token;
|
|
262
|
+
if (!fcmCredentials)
|
|
263
|
+
throw new Error('FCM not initialized');
|
|
264
|
+
const now = Math.floor(Date.now() / 1000);
|
|
265
|
+
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
266
|
+
const claims = Buffer.from(JSON.stringify({
|
|
267
|
+
iss: fcmCredentials.clientEmail,
|
|
268
|
+
scope: FCM_SCOPE,
|
|
269
|
+
aud: fcmCredentials.tokenUri,
|
|
270
|
+
iat: now,
|
|
271
|
+
exp: now + 3600,
|
|
272
|
+
})).toString('base64url');
|
|
273
|
+
const signer = createSign('RSA-SHA256');
|
|
274
|
+
signer.update(`${header}.${claims}`);
|
|
275
|
+
const assertion = `${header}.${claims}.${signer.sign(fcmCredentials.privateKey, 'base64url')}`;
|
|
276
|
+
const res = await fetch(fcmCredentials.tokenUri, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
279
|
+
body: new URLSearchParams({
|
|
280
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
281
|
+
assertion,
|
|
282
|
+
}),
|
|
283
|
+
signal: AbortSignal.timeout(15_000),
|
|
284
|
+
});
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
const body = await res.text().catch(() => '');
|
|
287
|
+
throw new Error(`FCM token exchange failed: ${res.status} ${body.slice(0, 200)}`);
|
|
288
|
+
}
|
|
289
|
+
const granted = (await res.json());
|
|
290
|
+
if (!granted.access_token)
|
|
291
|
+
throw new Error('FCM token exchange returned no access_token');
|
|
292
|
+
cachedFcmToken = {
|
|
293
|
+
token: granted.access_token,
|
|
294
|
+
expires: Date.now() + ((granted.expires_in ?? 3600) - 60) * 1000,
|
|
295
|
+
};
|
|
296
|
+
return cachedFcmToken.token;
|
|
297
|
+
}
|
|
298
|
+
/** Send a single FCM push and handle the response. */
|
|
299
|
+
async function sendToFcm(token, accessToken, original) {
|
|
300
|
+
// Data-only, deliberately: a `notification` block has the Android SDK draw
|
|
301
|
+
// the alert itself while the app is backgrounded, which skips the client's
|
|
302
|
+
// handler and with it the tap destination it derives from `type` and `uri`.
|
|
303
|
+
// FCM only carries strings in `data`, so everything is sent as one.
|
|
304
|
+
const data = { title: original.title, body: original.body };
|
|
305
|
+
for (const [key, value] of Object.entries(original.data ?? {})) {
|
|
306
|
+
data[key] = String(value);
|
|
307
|
+
}
|
|
308
|
+
const message = {
|
|
309
|
+
token,
|
|
310
|
+
data,
|
|
311
|
+
android: {
|
|
312
|
+
// Data-only messages are "normal" priority by default, which lets Doze
|
|
313
|
+
// hold one until the device next wakes. These are alerts somebody is
|
|
314
|
+
// waiting on, so they go high, the way APNs alert pushes do.
|
|
315
|
+
priority: 'HIGH',
|
|
316
|
+
...(original.collapseId ? { collapse_key: original.collapseId } : {}),
|
|
317
|
+
},
|
|
318
|
+
// `badge` has no FCM equivalent — an Android launcher badges from the
|
|
319
|
+
// notification the client posts, not from the payload.
|
|
320
|
+
};
|
|
321
|
+
const res = await fetch(`https://fcm.googleapis.com/v1/projects/${fcmCredentials.projectId}/messages:send`, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: {
|
|
324
|
+
authorization: `Bearer ${accessToken}`,
|
|
325
|
+
'content-type': 'application/json',
|
|
326
|
+
},
|
|
327
|
+
body: JSON.stringify({ message }),
|
|
328
|
+
signal: AbortSignal.timeout(15_000),
|
|
329
|
+
});
|
|
330
|
+
if (res.ok) {
|
|
331
|
+
emit('push', 'sent', { did: original.did, transport: 'fcm', token: token.slice(0, 8) + '...' });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const body = await res.text().catch(() => '');
|
|
335
|
+
// Google's word for a token that no longer belongs to an install — the same
|
|
336
|
+
// fact Apple reports as a 410.
|
|
337
|
+
if (res.status === 404 || body.includes('UNREGISTERED')) {
|
|
338
|
+
await removeToken(token).catch(() => { });
|
|
339
|
+
emit('push', 'token_removed', { did: original.did, transport: 'fcm', reason: 'unregistered' });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// The bearer token was refused: drop it so the next send mints a fresh one
|
|
343
|
+
// rather than replaying a credential Google has already rejected.
|
|
344
|
+
if (res.status === 401 || res.status === 403) {
|
|
345
|
+
cachedFcmToken = null;
|
|
346
|
+
}
|
|
347
|
+
emit('push', 'send_error', {
|
|
348
|
+
did: original.did,
|
|
349
|
+
transport: 'fcm',
|
|
350
|
+
status: res.status,
|
|
351
|
+
body: body.slice(0, 200),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/** Register a push token for a DID. Upserts on conflict. */
|
|
355
|
+
export async function registerToken(did, token, platform) {
|
|
356
|
+
await runSQL(`INSERT INTO _push_tokens (did, token, platform, created_at)
|
|
357
|
+
VALUES ($1, $2, $3, $4)
|
|
358
|
+
ON CONFLICT (did, token) DO UPDATE SET platform = excluded.platform`, [did, token, platform, new Date().toISOString()]);
|
|
359
|
+
}
|
|
360
|
+
/** Remove a push token. */
|
|
361
|
+
export async function removeToken(token) {
|
|
362
|
+
await runSQL(`DELETE FROM _push_tokens WHERE token = $1`, [token]);
|
|
363
|
+
}
|
|
364
|
+
/** Unregister a specific token for a DID. */
|
|
365
|
+
export async function unregisterToken(did, token) {
|
|
366
|
+
await runSQL(`DELETE FROM _push_tokens WHERE did = $1 AND token = $2`, [did, token]);
|
|
367
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SSRManifest {
|
|
2
|
+
getPreloadTags(url: string): string;
|
|
3
|
+
}
|
|
4
|
+
export interface RenderResult {
|
|
5
|
+
html: string;
|
|
6
|
+
head?: string;
|
|
7
|
+
}
|
|
8
|
+
export type RendererHandler = (request: Request, manifest: SSRManifest) => Promise<RenderResult>;
|
|
9
|
+
export declare function defineRenderer(handler: RendererHandler): {
|
|
10
|
+
__type: "renderer";
|
|
11
|
+
handler: RendererHandler;
|
|
12
|
+
};
|
|
13
|
+
export declare function registerRenderer(handler: RendererHandler): void;
|
|
14
|
+
export declare function setSSRManifest(manifest: SSRManifest): void;
|
|
15
|
+
export declare function getRenderer(): RendererHandler | null;
|
|
16
|
+
export declare function getSSRManifest(): SSRManifest | null;
|
|
17
|
+
/**
|
|
18
|
+
* Render an HTML page by calling the user's renderer and assembling the result
|
|
19
|
+
* into the index.html template.
|
|
20
|
+
*
|
|
21
|
+
* @param template - The index.html content (with <!--ssr-outlet--> placeholder)
|
|
22
|
+
* @param request - The incoming Request
|
|
23
|
+
* @param ogMeta - Optional OG meta tags to inject
|
|
24
|
+
* @returns Assembled HTML string, or null if no renderer is registered
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderPage(template: string, request: Request, ogMeta?: string | null): Promise<string | null>;
|
|
27
|
+
//# sourceMappingURL=renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;AAKhG,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe;;;EAEtD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAG/D;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAE1D;AAED,wBAAgB,WAAW,IAAI,eAAe,GAAG,IAAI,CAEpD;AAED,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAEnD;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsBnH"}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
let renderer = null;
|
|
3
|
+
let ssrManifest = null;
|
|
4
|
+
export function defineRenderer(handler) {
|
|
5
|
+
return { __type: 'renderer', handler };
|
|
6
|
+
}
|
|
7
|
+
export function registerRenderer(handler) {
|
|
8
|
+
renderer = handler;
|
|
9
|
+
log('[renderer] SSR renderer registered');
|
|
10
|
+
}
|
|
11
|
+
export function setSSRManifest(manifest) {
|
|
12
|
+
ssrManifest = manifest;
|
|
13
|
+
}
|
|
14
|
+
export function getRenderer() {
|
|
15
|
+
return renderer;
|
|
16
|
+
}
|
|
17
|
+
export function getSSRManifest() {
|
|
18
|
+
return ssrManifest;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Render an HTML page by calling the user's renderer and assembling the result
|
|
22
|
+
* into the index.html template.
|
|
23
|
+
*
|
|
24
|
+
* @param template - The index.html content (with <!--ssr-outlet--> placeholder)
|
|
25
|
+
* @param request - The incoming Request
|
|
26
|
+
* @param ogMeta - Optional OG meta tags to inject
|
|
27
|
+
* @returns Assembled HTML string, or null if no renderer is registered
|
|
28
|
+
*/
|
|
29
|
+
export async function renderPage(template, request, ogMeta) {
|
|
30
|
+
if (!renderer)
|
|
31
|
+
return null;
|
|
32
|
+
const manifest = ssrManifest || { getPreloadTags: () => '' };
|
|
33
|
+
const result = await renderer(request, manifest);
|
|
34
|
+
let html = template;
|
|
35
|
+
// Inject SSR head tags (preloads, styles)
|
|
36
|
+
if (result.head) {
|
|
37
|
+
html = html.replace('</head>', `${result.head}\n</head>`);
|
|
38
|
+
}
|
|
39
|
+
// Inject OG meta tags
|
|
40
|
+
if (ogMeta) {
|
|
41
|
+
html = html.replace('</head>', `${ogMeta}\n</head>`);
|
|
42
|
+
}
|
|
43
|
+
// Inject rendered HTML into the outlet
|
|
44
|
+
html = html.replace('<!--ssr-outlet-->', result.html);
|
|
45
|
+
return html;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-hatk.d.ts","sourceRoot":"","sources":["../src/resolve-hatk.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { registerHooks } from 'node:module';
|
|
4
|
+
/**
|
|
5
|
+
* Register a Node.js module resolve hook so dynamic import() of server files
|
|
6
|
+
* can resolve the $hatk alias to the generated entry points.
|
|
7
|
+
*/
|
|
8
|
+
export function registerHatkResolveHook() {
|
|
9
|
+
const hatkUrl = pathToFileURL(resolve('hatk.generated.ts')).href;
|
|
10
|
+
const hatkClientUrl = pathToFileURL(resolve('hatk.generated.client.ts')).href;
|
|
11
|
+
registerHooks({
|
|
12
|
+
resolve(specifier, context, nextResolve) {
|
|
13
|
+
if (specifier === '$hatk/client')
|
|
14
|
+
return { url: hatkClientUrl, shortCircuit: true };
|
|
15
|
+
if (specifier === '$hatk')
|
|
16
|
+
return { url: hatkUrl, shortCircuit: true };
|
|
17
|
+
return nextResolve(specifier, context);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a JSON Response with optional gzip compression.
|
|
3
|
+
* Mirrors the old jsonResponse/sendJson behavior.
|
|
4
|
+
*/
|
|
5
|
+
export declare function json(data: unknown, status?: number, acceptEncoding?: string | null): Response;
|
|
6
|
+
/** Create a JSON error Response. */
|
|
7
|
+
export declare function jsonError(status: number, message: string, acceptEncoding?: string | null): Response;
|
|
8
|
+
/** CORS preflight Response. */
|
|
9
|
+
export declare function cors(): Response;
|
|
10
|
+
/** Add CORS headers to an existing Response. */
|
|
11
|
+
export declare function withCors(response: Response): Response;
|
|
12
|
+
/** Create a static file Response with correct MIME type. */
|
|
13
|
+
export declare function file(content: Buffer | Uint8Array, contentType: string, cacheControl?: string): Response;
|
|
14
|
+
/** 404 Not Found. */
|
|
15
|
+
export declare function notFound(): Response;
|
|
16
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAuB1F;AAED,oCAAoC;AACpC,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAEnG;AAED,+BAA+B;AAC/B,wBAAgB,IAAI,IAAI,QAAQ,CAS/B;AAED,gDAAgD;AAChD,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAUrD;AAED,4DAA4D;AAC5D,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,CAQvG;AAED,qBAAqB;AACrB,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC"}
|
package/dist/response.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib';
|
|
2
|
+
import { normalizeValue } from "./database/db.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create a JSON Response with optional gzip compression.
|
|
5
|
+
* Mirrors the old jsonResponse/sendJson behavior.
|
|
6
|
+
*/
|
|
7
|
+
export function json(data, status = 200, acceptEncoding) {
|
|
8
|
+
const body = Buffer.from(JSON.stringify(data, (_, v) => normalizeValue(v)));
|
|
9
|
+
if (body.length > 1024 && acceptEncoding && /\bgzip\b/.test(acceptEncoding)) {
|
|
10
|
+
const compressed = gzipSync(body);
|
|
11
|
+
return new Response(compressed, {
|
|
12
|
+
status,
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'Content-Encoding': 'gzip',
|
|
16
|
+
Vary: 'Accept-Encoding',
|
|
17
|
+
...(status === 200 ? { 'Cache-Control': 'no-store' } : {}),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return new Response(body, {
|
|
22
|
+
status,
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
...(status === 200 ? { 'Cache-Control': 'no-store' } : {}),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Create a JSON error Response. */
|
|
30
|
+
export function jsonError(status, message, acceptEncoding) {
|
|
31
|
+
return json({ error: message }, status, acceptEncoding);
|
|
32
|
+
}
|
|
33
|
+
/** CORS preflight Response. */
|
|
34
|
+
export function cors() {
|
|
35
|
+
return new Response(null, {
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: {
|
|
38
|
+
'Access-Control-Allow-Origin': '*',
|
|
39
|
+
'Access-Control-Allow-Headers': '*',
|
|
40
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Add CORS headers to an existing Response. */
|
|
45
|
+
export function withCors(response) {
|
|
46
|
+
const headers = new Headers(response.headers);
|
|
47
|
+
headers.set('Access-Control-Allow-Origin', '*');
|
|
48
|
+
headers.set('Access-Control-Allow-Headers', '*');
|
|
49
|
+
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
50
|
+
return new Response(response.body, {
|
|
51
|
+
status: response.status,
|
|
52
|
+
statusText: response.statusText,
|
|
53
|
+
headers,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Create a static file Response with correct MIME type. */
|
|
57
|
+
export function file(content, contentType, cacheControl) {
|
|
58
|
+
return new Response(Buffer.from(content), {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': contentType,
|
|
62
|
+
...(cacheControl ? { 'Cache-Control': cacheControl } : {}),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** 404 Not Found. */
|
|
67
|
+
export function notFound() {
|
|
68
|
+
return new Response('Not Found', { status: 404 });
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ScannedModule {
|
|
2
|
+
path: string;
|
|
3
|
+
name: string;
|
|
4
|
+
mod: any;
|
|
5
|
+
}
|
|
6
|
+
export interface ScanResult {
|
|
7
|
+
feeds: ScannedModule[];
|
|
8
|
+
queries: ScannedModule[];
|
|
9
|
+
procedures: ScannedModule[];
|
|
10
|
+
hooks: ScannedModule[];
|
|
11
|
+
setup: ScannedModule[];
|
|
12
|
+
labels: ScannedModule[];
|
|
13
|
+
og: ScannedModule[];
|
|
14
|
+
renderer: ScannedModule | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Scan a directory for hatk server modules.
|
|
18
|
+
* Each file's default export is inspected for a `__type` tag.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scanServerDir(serverDir: string): Promise<ScanResult>;
|
|
21
|
+
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,GAAG,CAAA;CACT;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,UAAU,EAAE,aAAa,EAAE,CAAA;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB,EAAE,EAAE,aAAa,EAAE,CAAA;IACnB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;CAC/B;AAmBD;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA2D1E"}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { resolve, relative } from 'node:path';
|
|
10
|
+
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
11
|
+
import { log } from "./logger.js";
|
|
12
|
+
/** Recursively collect .ts/.js files, skipping _ prefixed and dot files */
|
|
13
|
+
function walkDir(dir) {
|
|
14
|
+
const results = [];
|
|
15
|
+
try {
|
|
16
|
+
for (const entry of readdirSync(dir)) {
|
|
17
|
+
if (entry.startsWith('_') || entry.startsWith('.'))
|
|
18
|
+
continue;
|
|
19
|
+
const full = resolve(dir, entry);
|
|
20
|
+
if (statSync(full).isDirectory()) {
|
|
21
|
+
results.push(...walkDir(full));
|
|
22
|
+
}
|
|
23
|
+
else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
|
|
24
|
+
results.push(full);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
return results.sort();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scan a directory for hatk server modules.
|
|
33
|
+
* Each file's default export is inspected for a `__type` tag.
|
|
34
|
+
*/
|
|
35
|
+
export async function scanServerDir(serverDir) {
|
|
36
|
+
const result = {
|
|
37
|
+
feeds: [],
|
|
38
|
+
queries: [],
|
|
39
|
+
procedures: [],
|
|
40
|
+
hooks: [],
|
|
41
|
+
setup: [],
|
|
42
|
+
labels: [],
|
|
43
|
+
og: [],
|
|
44
|
+
renderer: null,
|
|
45
|
+
};
|
|
46
|
+
if (!existsSync(serverDir))
|
|
47
|
+
return result;
|
|
48
|
+
const files = walkDir(serverDir);
|
|
49
|
+
for (const filePath of files) {
|
|
50
|
+
const name = relative(serverDir, filePath).replace(/\.(ts|js)$/, '');
|
|
51
|
+
const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${filePath}?t=${Date.now()}`));
|
|
52
|
+
const exported = mod.default;
|
|
53
|
+
if (!exported) {
|
|
54
|
+
log(`[scanner] ${name}: no default export, skipping`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const entry = { path: filePath, name, mod: exported };
|
|
58
|
+
switch (exported.__type) {
|
|
59
|
+
case 'feed':
|
|
60
|
+
result.feeds.push(entry);
|
|
61
|
+
break;
|
|
62
|
+
case 'query':
|
|
63
|
+
result.queries.push(entry);
|
|
64
|
+
break;
|
|
65
|
+
case 'procedure':
|
|
66
|
+
result.procedures.push(entry);
|
|
67
|
+
break;
|
|
68
|
+
case 'hook':
|
|
69
|
+
result.hooks.push(entry);
|
|
70
|
+
break;
|
|
71
|
+
case 'setup':
|
|
72
|
+
result.setup.push(entry);
|
|
73
|
+
break;
|
|
74
|
+
case 'labels':
|
|
75
|
+
result.labels.push(entry);
|
|
76
|
+
break;
|
|
77
|
+
case 'og':
|
|
78
|
+
result.og.push(entry);
|
|
79
|
+
break;
|
|
80
|
+
case 'renderer':
|
|
81
|
+
result.renderer = entry;
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
log(`[scanner] ${name}: no recognized __type tag, skipping`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|