@trypattern/sdk 0.1.8 → 0.1.10
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 +37 -8
- package/dist/app.d.ts +1 -0
- package/dist/app.js +4 -3
- package/dist/cli/app.js +120 -7
- package/dist/cli/publishing.js +3 -1
- package/dist/components/AppFrame.d.ts +5 -1
- package/dist/components/AppFrame.js +25 -7
- package/dist/components/Appear.d.ts +4 -1
- package/dist/components/Appear.js +82 -3
- package/dist/components/Card.d.ts +7 -2
- package/dist/components/Card.js +59 -3
- package/dist/components/Checkbox.d.ts +10 -0
- package/dist/components/Checkbox.js +59 -0
- package/dist/components/Choice.d.ts +15 -0
- package/dist/components/Choice.js +80 -0
- package/dist/components/ChoiceGroup.d.ts +11 -0
- package/dist/components/ChoiceGroup.js +44 -0
- package/dist/components/Icon.d.ts +2 -1
- package/dist/components/Icon.js +2 -2
- package/dist/components/Image.d.ts +11 -0
- package/dist/components/Image.js +76 -0
- package/dist/components/Link.js +8 -3
- package/dist/components/ListRow.d.ts +2 -1
- package/dist/components/ListRow.js +7 -4
- package/dist/components/PlainScreen.d.ts +16 -0
- package/dist/components/PlainScreen.js +80 -0
- package/dist/components/PlainSidebar.d.ts +15 -0
- package/dist/components/PlainSidebar.js +179 -0
- package/dist/components/Screen.d.ts +3 -1
- package/dist/components/Screen.js +5 -1
- package/dist/components/Sidebar.js +2 -1
- package/dist/components/TextField.d.ts +3 -1
- package/dist/components/TextField.js +25 -4
- package/dist/components/choiceContext.d.ts +4 -0
- package/dist/components/choiceContext.js +7 -0
- package/dist/components/frame.d.ts +2 -0
- package/dist/components/icons.d.ts +3 -0
- package/dist/components/icons.js +4 -1
- package/dist/components/interaction.d.ts +1 -1
- package/dist/entityStore.d.ts +1 -0
- package/dist/entityStore.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +36 -0
- package/dist/media.js +74 -0
- package/dist/patternAccess.d.ts +21 -0
- package/dist/patternAccess.js +110 -0
- package/dist/publishWire.d.ts +318 -0
- package/dist/publishWire.js +128 -1
- package/dist/react.d.ts +5 -0
- package/dist/react.js +4 -0
- package/dist/rules/checkApiRouting.js +1 -1
- package/dist/rules/lintPlugin.js +13 -4
- package/dist/rules/messages.d.ts +1 -0
- package/dist/rules/messages.js +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/createServer.d.ts +92 -6
- package/dist/server/createServer.js +38 -2
- package/dist/server/database.d.ts +74 -0
- package/dist/server/database.js +93 -0
- package/dist/server/env.d.ts +3 -1
- package/dist/server/imageHeader.d.ts +10 -0
- package/dist/server/imageHeader.js +159 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +3 -0
- package/dist/server/jsonLd.d.ts +45 -0
- package/dist/server/jsonLd.js +88 -0
- package/dist/server/keyset.d.ts +13 -0
- package/dist/server/keyset.js +52 -0
- package/dist/server/media.d.ts +16 -0
- package/dist/server/media.js +135 -0
- package/dist/server/pages.d.ts +72 -0
- package/dist/server/pages.js +301 -0
- package/dist/server/patternKeys.d.ts +1 -0
- package/dist/server/patternKeys.js +3 -0
- package/dist/server/patternViewer.d.ts +6 -0
- package/dist/server/patternViewer.js +27 -0
- package/dist/server/services.js +1 -1
- package/dist/server/tables.d.ts +19 -3
- package/dist/server/tables.js +38 -14
- package/dist/serviceClients.js +3 -5
- package/dist/start.js +26 -1
- package/dist/table.d.ts +1 -0
- package/dist/table.js +8 -1
- package/dist/tables.d.ts +2 -1
- package/dist/tables.js +10 -1
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import type { Tables } from '../table.ts';
|
|
3
3
|
import type { ServerBindings, ServerEnv } from './env.ts';
|
|
4
|
-
|
|
4
|
+
import type { Pages } from './pages.ts';
|
|
5
|
+
export declare function createServer<Group extends Tables, Bindings extends ServerBindings = ServerBindings>({ tables, admins, routes, pages }: {
|
|
5
6
|
tables: Group;
|
|
6
7
|
admins: readonly string[];
|
|
7
8
|
routes?: Hono<ServerEnv<Bindings>>;
|
|
8
|
-
|
|
9
|
+
pages?: Pages;
|
|
10
|
+
}): import("hono/hono-base").HonoBase<ServerEnv, import("hono/types").BlankSchema | import("hono/types").MergeSchemaPath<(((({
|
|
9
11
|
"/api/health": {
|
|
10
12
|
$get: {
|
|
11
13
|
input: {};
|
|
@@ -172,8 +174,90 @@ export declare function createServer<Group extends Tables, Bindings extends Serv
|
|
|
172
174
|
};
|
|
173
175
|
};
|
|
174
176
|
}) | import("hono/types").MergeSchemaPath<{
|
|
177
|
+
"/schema": {
|
|
178
|
+
$get: {
|
|
179
|
+
input: {};
|
|
180
|
+
output: {
|
|
181
|
+
error: string;
|
|
182
|
+
};
|
|
183
|
+
outputFormat: "json";
|
|
184
|
+
status: 401;
|
|
185
|
+
} | {
|
|
186
|
+
input: {};
|
|
187
|
+
output: {
|
|
188
|
+
error: string;
|
|
189
|
+
};
|
|
190
|
+
outputFormat: "json";
|
|
191
|
+
status: 403;
|
|
192
|
+
} | {
|
|
193
|
+
input: {};
|
|
194
|
+
output: {
|
|
195
|
+
tables: {
|
|
196
|
+
name: string;
|
|
197
|
+
schema: {
|
|
198
|
+
[x: string]: import("hono/utils/types").JSONValue;
|
|
199
|
+
};
|
|
200
|
+
access: {
|
|
201
|
+
read: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
202
|
+
create: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
203
|
+
update: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
204
|
+
delete: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
205
|
+
};
|
|
206
|
+
public: string[];
|
|
207
|
+
rowCount: number;
|
|
208
|
+
isBuiltIn: boolean;
|
|
209
|
+
}[];
|
|
210
|
+
};
|
|
211
|
+
outputFormat: "json";
|
|
212
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
} & {
|
|
216
|
+
"/tables/users": {
|
|
217
|
+
$get: {
|
|
218
|
+
input: {};
|
|
219
|
+
output: {
|
|
220
|
+
error: string;
|
|
221
|
+
};
|
|
222
|
+
outputFormat: "json";
|
|
223
|
+
status: 401;
|
|
224
|
+
} | {
|
|
225
|
+
input: {};
|
|
226
|
+
output: {
|
|
227
|
+
error: string;
|
|
228
|
+
};
|
|
229
|
+
outputFormat: "json";
|
|
230
|
+
status: 403;
|
|
231
|
+
} | {
|
|
232
|
+
input: {};
|
|
233
|
+
output: readonly {
|
|
234
|
+
readonly [x: string]: import("hono/utils/types").JSONValue;
|
|
235
|
+
}[];
|
|
236
|
+
outputFormat: "json";
|
|
237
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
238
|
+
} | {
|
|
239
|
+
input: {};
|
|
240
|
+
output: {
|
|
241
|
+
error: string;
|
|
242
|
+
};
|
|
243
|
+
outputFormat: "json";
|
|
244
|
+
status: 400;
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
}, "/api"> | import("hono/types").MergeSchemaPath<{
|
|
175
248
|
"/:table": {
|
|
176
249
|
$get: {
|
|
250
|
+
input: {
|
|
251
|
+
param: {
|
|
252
|
+
table: string;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
output: readonly {
|
|
256
|
+
readonly [x: string]: import("hono/utils/types").JSONValue;
|
|
257
|
+
}[];
|
|
258
|
+
outputFormat: "json";
|
|
259
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
260
|
+
} | {
|
|
177
261
|
input: {
|
|
178
262
|
param: {
|
|
179
263
|
table: string;
|
|
@@ -190,9 +274,11 @@ export declare function createServer<Group extends Tables, Bindings extends Serv
|
|
|
190
274
|
table: string;
|
|
191
275
|
};
|
|
192
276
|
};
|
|
193
|
-
output: {
|
|
277
|
+
output: {
|
|
278
|
+
error: string;
|
|
279
|
+
};
|
|
194
280
|
outputFormat: "json";
|
|
195
|
-
status:
|
|
281
|
+
status: 404;
|
|
196
282
|
} | {
|
|
197
283
|
input: {
|
|
198
284
|
param: {
|
|
@@ -203,7 +289,7 @@ export declare function createServer<Group extends Tables, Bindings extends Serv
|
|
|
203
289
|
error: string;
|
|
204
290
|
};
|
|
205
291
|
outputFormat: "json";
|
|
206
|
-
status:
|
|
292
|
+
status: 400;
|
|
207
293
|
};
|
|
208
294
|
};
|
|
209
295
|
} & {
|
|
@@ -399,4 +485,4 @@ export declare function createServer<Group extends Tables, Bindings extends Serv
|
|
|
399
485
|
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
400
486
|
};
|
|
401
487
|
};
|
|
402
|
-
}, "/api/tables">, "/
|
|
488
|
+
}, "/api/tables">, "/">, "/", "/">;
|
|
@@ -2,10 +2,15 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { csrf } from 'hono/csrf';
|
|
3
3
|
import { adminEmails } from "../access.js";
|
|
4
4
|
import { appTitle } from "../appTitle.js";
|
|
5
|
+
import { media } from "../media.js";
|
|
5
6
|
import { auth, devSignIn } from "./auth.js";
|
|
7
|
+
import { serveDatabase } from "./database.js";
|
|
8
|
+
import { attachPatternViewer, bearerOf } from "./patternViewer.js";
|
|
6
9
|
import { forgetSession, issuePass, needsRecheck, readPass, rememberUser } from "./session.js";
|
|
7
10
|
import { renewSession } from "./sessions.js";
|
|
8
11
|
import { readTitle, writeTitle } from "./settings.js";
|
|
12
|
+
import { serveMedia, uploadMedia } from "./media.js";
|
|
13
|
+
import { matchPage, renderPage, robots, sitemap, wantsHtml } from "./pages.js";
|
|
9
14
|
import { serveTables } from "./tables.js";
|
|
10
15
|
import { findUser } from "./users.js";
|
|
11
16
|
const PUBLIC_PATHS = ['/api/health', '/api/auth/'];
|
|
@@ -19,6 +24,9 @@ function knowAdmins(admins) {
|
|
|
19
24
|
};
|
|
20
25
|
}
|
|
21
26
|
async function attachViewer(c, next) {
|
|
27
|
+
const bearer = bearerOf(c);
|
|
28
|
+
if (bearer !== undefined)
|
|
29
|
+
return attachPatternViewer(c, next, bearer);
|
|
22
30
|
if (PUBLIC_PATHS.some((path) => c.req.path.startsWith(path)))
|
|
23
31
|
return next();
|
|
24
32
|
const isOptional = c.req.path === APP_PATH ||
|
|
@@ -42,8 +50,9 @@ async function attachViewer(c, next) {
|
|
|
42
50
|
});
|
|
43
51
|
return next();
|
|
44
52
|
}
|
|
45
|
-
export function createServer({ tables, admins, routes }) {
|
|
53
|
+
export function createServer({ tables, admins, routes, pages }) {
|
|
46
54
|
const authRoutes = import.meta.env.DEV ? auth.route('/', devSignIn) : auth;
|
|
55
|
+
const hasMedia = Object.values(tables).some((table) => table === media);
|
|
47
56
|
const server = new Hono()
|
|
48
57
|
.basePath('/api')
|
|
49
58
|
.use(csrf())
|
|
@@ -76,7 +85,34 @@ export function createServer({ tables, admins, routes }) {
|
|
|
76
85
|
rememberUser(c, user);
|
|
77
86
|
return c.json({ ...user, isAdmin: viewer?.isAdmin ?? false }, 200);
|
|
78
87
|
})
|
|
88
|
+
.route('/', serveDatabase(tables))
|
|
79
89
|
.route('/tables', serveTables(tables));
|
|
90
|
+
if (hasMedia)
|
|
91
|
+
server.post('/media', uploadMedia);
|
|
80
92
|
const own = routes;
|
|
81
|
-
|
|
93
|
+
const root = new Hono();
|
|
94
|
+
if (hasMedia)
|
|
95
|
+
root.get('/media/*', serveMedia);
|
|
96
|
+
if (pages !== undefined) {
|
|
97
|
+
root.get('/sitemap.xml', (c) => sitemap(c, pages));
|
|
98
|
+
root.get('/robots.txt', robots);
|
|
99
|
+
}
|
|
100
|
+
return root
|
|
101
|
+
.route('/', own === undefined ? server : server.route('/', own))
|
|
102
|
+
.notFound((c) => {
|
|
103
|
+
if (c.req.path === '/api' || c.req.path.startsWith('/api/')) {
|
|
104
|
+
return c.json({ error: `No route for ${c.req.path}` }, 404);
|
|
105
|
+
}
|
|
106
|
+
const page = pages === undefined || !wantsHtml(c)
|
|
107
|
+
? undefined
|
|
108
|
+
: matchPage(pages, c.req.path);
|
|
109
|
+
if (page !== undefined && pages !== undefined) {
|
|
110
|
+
return renderPage(c, pages, page.route, page.params);
|
|
111
|
+
}
|
|
112
|
+
const assets = c.env.ASSETS;
|
|
113
|
+
if (assets === undefined) {
|
|
114
|
+
return c.json({ error: `No route for ${c.req.path}` }, 404);
|
|
115
|
+
}
|
|
116
|
+
return assets.fetch(c.req.raw);
|
|
117
|
+
});
|
|
82
118
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Tables } from '../table.ts';
|
|
2
|
+
import type { ServerEnv } from './env.ts';
|
|
3
|
+
export declare function serveDatabase<Group extends Tables>(tables: Group): import("hono/hono-base").HonoBase<ServerEnv, {
|
|
4
|
+
"/schema": {
|
|
5
|
+
$get: {
|
|
6
|
+
input: {};
|
|
7
|
+
output: {
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
outputFormat: "json";
|
|
11
|
+
status: 401;
|
|
12
|
+
} | {
|
|
13
|
+
input: {};
|
|
14
|
+
output: {
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
outputFormat: "json";
|
|
18
|
+
status: 403;
|
|
19
|
+
} | {
|
|
20
|
+
input: {};
|
|
21
|
+
output: {
|
|
22
|
+
tables: {
|
|
23
|
+
name: string;
|
|
24
|
+
schema: {
|
|
25
|
+
[x: string]: import("hono/utils/types").JSONValue;
|
|
26
|
+
};
|
|
27
|
+
access: {
|
|
28
|
+
read: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
29
|
+
create: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
30
|
+
update: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
31
|
+
delete: "anyone" | "signedIn" | "owner" | "admin" | "server";
|
|
32
|
+
};
|
|
33
|
+
public: string[];
|
|
34
|
+
rowCount: number;
|
|
35
|
+
isBuiltIn: boolean;
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
outputFormat: "json";
|
|
39
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
} & {
|
|
43
|
+
"/tables/users": {
|
|
44
|
+
$get: {
|
|
45
|
+
input: {};
|
|
46
|
+
output: {
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
outputFormat: "json";
|
|
50
|
+
status: 401;
|
|
51
|
+
} | {
|
|
52
|
+
input: {};
|
|
53
|
+
output: {
|
|
54
|
+
error: string;
|
|
55
|
+
};
|
|
56
|
+
outputFormat: "json";
|
|
57
|
+
status: 403;
|
|
58
|
+
} | {
|
|
59
|
+
input: {};
|
|
60
|
+
output: readonly {
|
|
61
|
+
readonly [x: string]: import("hono/utils/types").JSONValue;
|
|
62
|
+
}[];
|
|
63
|
+
outputFormat: "json";
|
|
64
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
65
|
+
} | {
|
|
66
|
+
input: {};
|
|
67
|
+
output: {
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
outputFormat: "json";
|
|
71
|
+
status: 400;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
}, "/", "/tables/users">;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { asc, count } from 'drizzle-orm';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { PAGE_PROBLEM, afterCursor, pageOf, pageReply } from "./keyset.js";
|
|
6
|
+
import { sqlTable } from "./tables.js";
|
|
7
|
+
import { users } from "./users.js";
|
|
8
|
+
const userRow = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
email: z.string(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
avatarUrl: z.string().nullable(),
|
|
13
|
+
createdAt: z.string()
|
|
14
|
+
});
|
|
15
|
+
const USERS_ACCESS = {
|
|
16
|
+
read: 'admin',
|
|
17
|
+
create: 'server',
|
|
18
|
+
update: 'server',
|
|
19
|
+
delete: 'server'
|
|
20
|
+
};
|
|
21
|
+
function adminRefusal(c) {
|
|
22
|
+
const viewer = c.get('viewer');
|
|
23
|
+
if (viewer === null)
|
|
24
|
+
return c.json({ error: 'Signed out' }, 401);
|
|
25
|
+
return viewer.isAdmin ? undefined : c.json({ error: 'forbidden' }, 403);
|
|
26
|
+
}
|
|
27
|
+
function jsonSchemaOf(schema) {
|
|
28
|
+
return z.toJSONSchema(schema, { unrepresentable: 'any' });
|
|
29
|
+
}
|
|
30
|
+
async function rowCount(c, table) {
|
|
31
|
+
const counted = await drizzle(c.env.DB)
|
|
32
|
+
.select({ count: count() })
|
|
33
|
+
.from(sqlTable(table))
|
|
34
|
+
.get();
|
|
35
|
+
return counted?.count ?? 0;
|
|
36
|
+
}
|
|
37
|
+
async function userCount(c) {
|
|
38
|
+
const counted = await drizzle(c.env.DB)
|
|
39
|
+
.select({ count: count() })
|
|
40
|
+
.from(users)
|
|
41
|
+
.get();
|
|
42
|
+
return counted?.count ?? 0;
|
|
43
|
+
}
|
|
44
|
+
export function serveDatabase(tables) {
|
|
45
|
+
const listed = Object.values(tables).filter((table) => table.access.read !== 'server');
|
|
46
|
+
return new Hono()
|
|
47
|
+
.get('/schema', async (c) => {
|
|
48
|
+
const refusal = adminRefusal(c);
|
|
49
|
+
if (refusal !== undefined)
|
|
50
|
+
return refusal;
|
|
51
|
+
const own = await Promise.all(listed.map(async (table) => ({
|
|
52
|
+
name: table.name,
|
|
53
|
+
schema: jsonSchemaOf(table.row),
|
|
54
|
+
access: table.access,
|
|
55
|
+
public: [...table.public],
|
|
56
|
+
rowCount: await rowCount(c, table),
|
|
57
|
+
isBuiltIn: false
|
|
58
|
+
})));
|
|
59
|
+
const builtIn = {
|
|
60
|
+
name: 'users',
|
|
61
|
+
schema: jsonSchemaOf(userRow),
|
|
62
|
+
access: USERS_ACCESS,
|
|
63
|
+
public: [],
|
|
64
|
+
rowCount: await userCount(c),
|
|
65
|
+
isBuiltIn: true
|
|
66
|
+
};
|
|
67
|
+
return c.json({ tables: [...own, builtIn] });
|
|
68
|
+
})
|
|
69
|
+
.get('/tables/users', async (c) => {
|
|
70
|
+
const refusal = adminRefusal(c);
|
|
71
|
+
if (refusal !== undefined)
|
|
72
|
+
return refusal;
|
|
73
|
+
const page = pageOf(c);
|
|
74
|
+
if (page === 'invalid') {
|
|
75
|
+
return c.json({ error: PAGE_PROBLEM }, 400);
|
|
76
|
+
}
|
|
77
|
+
const query = drizzle(c.env.DB)
|
|
78
|
+
.select({
|
|
79
|
+
id: users.id,
|
|
80
|
+
email: users.email,
|
|
81
|
+
name: users.name,
|
|
82
|
+
avatarUrl: users.avatarUrl,
|
|
83
|
+
createdAt: users.createdAt
|
|
84
|
+
})
|
|
85
|
+
.from(users)
|
|
86
|
+
.where(afterCursor(users.createdAt, users.id, page))
|
|
87
|
+
.orderBy(asc(users.createdAt), asc(users.id));
|
|
88
|
+
const rows = await (page === undefined
|
|
89
|
+
? query
|
|
90
|
+
: query.limit(page.limit + 1));
|
|
91
|
+
return pageReply(c, rows, page);
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/server/env.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { D1Database } from '@cloudflare/workers-types';
|
|
1
|
+
import type { D1Database, Fetcher, R2Bucket } from '@cloudflare/workers-types';
|
|
2
2
|
import type { Viewer } from '../access.ts';
|
|
3
3
|
export type { Viewer };
|
|
4
4
|
export type ServerBindings = {
|
|
5
5
|
DB: D1Database;
|
|
6
6
|
SESSION_SECRET: string;
|
|
7
7
|
AUTH_SECRET?: string;
|
|
8
|
+
MEDIA?: R2Bucket;
|
|
9
|
+
ASSETS?: Fetcher;
|
|
8
10
|
};
|
|
9
11
|
export type ServerEnv<Bindings extends ServerBindings = ServerBindings> = {
|
|
10
12
|
Bindings: Bindings;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ImageFormat = {
|
|
2
|
+
type: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif' | 'image/avif';
|
|
3
|
+
extension: 'jpg' | 'png' | 'webp' | 'gif' | 'avif';
|
|
4
|
+
};
|
|
5
|
+
export type ImageHeader = ImageFormat & {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function isHeic(bytes: Uint8Array): boolean;
|
|
10
|
+
export declare function readImageHeader(bytes: Uint8Array): ImageHeader | undefined;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const SOF_MARKERS = new Set([
|
|
2
|
+
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf
|
|
3
|
+
]);
|
|
4
|
+
const ORIENTATION_TAG = 0x0112;
|
|
5
|
+
const ROTATED_ORIENTATIONS = new Set([5, 6, 7, 8]);
|
|
6
|
+
const HEIC_BRANDS = new Set([
|
|
7
|
+
'heic',
|
|
8
|
+
'heix',
|
|
9
|
+
'hevc',
|
|
10
|
+
'hevx',
|
|
11
|
+
'heim',
|
|
12
|
+
'heis',
|
|
13
|
+
'mif1',
|
|
14
|
+
'msf1'
|
|
15
|
+
]);
|
|
16
|
+
const AVIF_BRANDS = new Set(['avif', 'avis']);
|
|
17
|
+
function ascii(bytes, start, length) {
|
|
18
|
+
return String.fromCharCode(...bytes.subarray(start, start + length));
|
|
19
|
+
}
|
|
20
|
+
function brandsOf(bytes) {
|
|
21
|
+
if (bytes.length < 16 || ascii(bytes, 4, 4) !== 'ftyp')
|
|
22
|
+
return [];
|
|
23
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
24
|
+
const end = Math.min(bytes.length, view.getUint32(0));
|
|
25
|
+
const brands = [ascii(bytes, 8, 4)];
|
|
26
|
+
for (let offset = 16; offset + 4 <= end; offset += 4) {
|
|
27
|
+
brands.push(ascii(bytes, offset, 4));
|
|
28
|
+
}
|
|
29
|
+
return brands;
|
|
30
|
+
}
|
|
31
|
+
export function isHeic(bytes) {
|
|
32
|
+
const brands = brandsOf(bytes);
|
|
33
|
+
return (!brands.some((brand) => AVIF_BRANDS.has(brand)) &&
|
|
34
|
+
brands.some((brand) => HEIC_BRANDS.has(brand)));
|
|
35
|
+
}
|
|
36
|
+
function exifOrientation(view, start, end) {
|
|
37
|
+
if (end - start < 14)
|
|
38
|
+
return 1;
|
|
39
|
+
if (view.getUint32(start) !== 0x45786966 || view.getUint16(start + 4) !== 0)
|
|
40
|
+
return 1;
|
|
41
|
+
const tiff = start + 6;
|
|
42
|
+
const little = view.getUint16(tiff) === 0x4949;
|
|
43
|
+
const ifd = tiff + view.getUint32(tiff + 4, little);
|
|
44
|
+
if (ifd + 2 > end)
|
|
45
|
+
return 1;
|
|
46
|
+
const count = view.getUint16(ifd, little);
|
|
47
|
+
for (let index = 0; index < count; index += 1) {
|
|
48
|
+
const entry = ifd + 2 + index * 12;
|
|
49
|
+
if (entry + 12 > end)
|
|
50
|
+
return 1;
|
|
51
|
+
if (view.getUint16(entry, little) === ORIENTATION_TAG) {
|
|
52
|
+
return view.getUint16(entry + 8, little);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
function jpegSize(view) {
|
|
58
|
+
let orientation = 1;
|
|
59
|
+
let offset = 2;
|
|
60
|
+
while (offset + 4 <= view.byteLength) {
|
|
61
|
+
if (view.getUint8(offset) !== 0xff)
|
|
62
|
+
return undefined;
|
|
63
|
+
const marker = view.getUint8(offset + 1);
|
|
64
|
+
if (marker === 0xff) {
|
|
65
|
+
offset += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const length = view.getUint16(offset + 2);
|
|
69
|
+
if (marker === 0xe1) {
|
|
70
|
+
orientation = exifOrientation(view, offset + 4, Math.min(view.byteLength, offset + 2 + length));
|
|
71
|
+
}
|
|
72
|
+
if (SOF_MARKERS.has(marker) && offset + 9 <= view.byteLength) {
|
|
73
|
+
const height = view.getUint16(offset + 5);
|
|
74
|
+
const width = view.getUint16(offset + 7);
|
|
75
|
+
return ROTATED_ORIENTATIONS.has(orientation)
|
|
76
|
+
? { width: height, height: width }
|
|
77
|
+
: { width, height };
|
|
78
|
+
}
|
|
79
|
+
offset += 2 + length;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function webpSize(bytes, view) {
|
|
84
|
+
const chunk = ascii(bytes, 12, 4);
|
|
85
|
+
if (chunk === 'VP8X' && bytes.length >= 30) {
|
|
86
|
+
const width = 1 + (view.getUint32(24, true) & 0xffffff);
|
|
87
|
+
const height = 1 + (view.getUint32(27, true) & 0xffffff);
|
|
88
|
+
return { width, height };
|
|
89
|
+
}
|
|
90
|
+
if (chunk === 'VP8 ' && bytes.length >= 30) {
|
|
91
|
+
return {
|
|
92
|
+
width: view.getUint16(26, true) & 0x3fff,
|
|
93
|
+
height: view.getUint16(28, true) & 0x3fff
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (chunk === 'VP8L' && bytes.length >= 25) {
|
|
97
|
+
const bits = view.getUint32(21, true);
|
|
98
|
+
return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >> 14) & 0x3fff) };
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function avifSize(bytes, view) {
|
|
103
|
+
const limit = Math.min(bytes.length - 16, 65_536);
|
|
104
|
+
for (let offset = 4; offset < limit; offset += 1) {
|
|
105
|
+
if (ascii(bytes, offset, 4) === 'ispe') {
|
|
106
|
+
return {
|
|
107
|
+
width: view.getUint32(offset + 8),
|
|
108
|
+
height: view.getUint32(offset + 12)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
function sizeOf(bytes) {
|
|
115
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
116
|
+
if (bytes.length >= 4 && view.getUint16(0) === 0xffd8) {
|
|
117
|
+
const size = jpegSize(view);
|
|
118
|
+
return size && { type: 'image/jpeg', extension: 'jpg', ...size };
|
|
119
|
+
}
|
|
120
|
+
if (bytes.length >= 24 && ascii(bytes, 1, 3) === 'PNG') {
|
|
121
|
+
return {
|
|
122
|
+
type: 'image/png',
|
|
123
|
+
extension: 'png',
|
|
124
|
+
width: view.getUint32(16),
|
|
125
|
+
height: view.getUint32(20)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (bytes.length >= 10 && ascii(bytes, 0, 4) === 'GIF8') {
|
|
129
|
+
return {
|
|
130
|
+
type: 'image/gif',
|
|
131
|
+
extension: 'gif',
|
|
132
|
+
width: view.getUint16(6, true),
|
|
133
|
+
height: view.getUint16(8, true)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (bytes.length >= 16 &&
|
|
137
|
+
ascii(bytes, 0, 4) === 'RIFF' &&
|
|
138
|
+
ascii(bytes, 8, 4) === 'WEBP') {
|
|
139
|
+
const size = webpSize(bytes, view);
|
|
140
|
+
return size && { type: 'image/webp', extension: 'webp', ...size };
|
|
141
|
+
}
|
|
142
|
+
if (brandsOf(bytes).some((brand) => AVIF_BRANDS.has(brand))) {
|
|
143
|
+
const size = avifSize(bytes, view);
|
|
144
|
+
return size && { type: 'image/avif', extension: 'avif', ...size };
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
export function readImageHeader(bytes) {
|
|
149
|
+
try {
|
|
150
|
+
const found = sizeOf(bytes);
|
|
151
|
+
if (found === undefined || found.width < 1 || found.height < 1) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -10,4 +10,9 @@ export { readTitle, seedTitleSql, settings, writeTitle } from './settings.ts';
|
|
|
10
10
|
export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from './signInWire.ts';
|
|
11
11
|
export type { VerifiedIdentity } from './signInWire.ts';
|
|
12
12
|
export { findUser, users } from './users.ts';
|
|
13
|
-
export
|
|
13
|
+
export { MEDIA_LIMIT_BYTES } from '../media.ts';
|
|
14
|
+
export { localBusiness, product } from './jsonLd.ts';
|
|
15
|
+
export type { JsonLd, LocalBusinessFacts, OpeningHours, PostalAddress, ProductFacts, Weekday } from './jsonLd.ts';
|
|
16
|
+
export { definePages } from './pages.ts';
|
|
17
|
+
export type { PageDefinition, PageHead, PageHero, PageParams, PageRequest, Pages } from './pages.ts';
|
|
18
|
+
export type { D1Database, Fetcher, R2Bucket } from '@cloudflare/workers-types';
|
package/dist/server/index.js
CHANGED
|
@@ -7,3 +7,6 @@ export { deleteSession, sessions } from "./sessions.js";
|
|
|
7
7
|
export { readTitle, seedTitleSql, settings, writeTitle } from "./settings.js";
|
|
8
8
|
export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from "./signInWire.js";
|
|
9
9
|
export { findUser, users } from "./users.js";
|
|
10
|
+
export { MEDIA_LIMIT_BYTES } from "../media.js";
|
|
11
|
+
export { localBusiness, product } from "./jsonLd.js";
|
|
12
|
+
export { definePages } from "./pages.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type JsonLd = {
|
|
2
|
+
readonly '@context': 'https://schema.org';
|
|
3
|
+
readonly '@type': string;
|
|
4
|
+
} & Readonly<Record<string, unknown>>;
|
|
5
|
+
export type Weekday = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday';
|
|
6
|
+
export type OpeningHours = {
|
|
7
|
+
days: Weekday | readonly Weekday[];
|
|
8
|
+
opens: string;
|
|
9
|
+
closes: string;
|
|
10
|
+
};
|
|
11
|
+
export type PostalAddress = {
|
|
12
|
+
street?: string;
|
|
13
|
+
locality?: string;
|
|
14
|
+
region?: string;
|
|
15
|
+
postalCode?: string;
|
|
16
|
+
country?: string;
|
|
17
|
+
};
|
|
18
|
+
export type LocalBusinessFacts = {
|
|
19
|
+
name: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
image?: string | readonly string[];
|
|
23
|
+
description?: string;
|
|
24
|
+
telephone?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
address?: PostalAddress;
|
|
27
|
+
hours?: readonly OpeningHours[];
|
|
28
|
+
priceRange?: string;
|
|
29
|
+
sameAs?: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
export type ProductFacts = {
|
|
32
|
+
name: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
image?: string | readonly string[];
|
|
35
|
+
description?: string;
|
|
36
|
+
sku?: string;
|
|
37
|
+
brand?: string;
|
|
38
|
+
offer?: {
|
|
39
|
+
price: number;
|
|
40
|
+
currency: string;
|
|
41
|
+
availability?: 'InStock' | 'OutOfStock' | 'PreOrder' | 'BackOrder';
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare function localBusiness(facts: LocalBusinessFacts): JsonLd;
|
|
45
|
+
export declare function product(facts: ProductFacts): JsonLd;
|