palbase 0.0.0-stage → 0.2.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 +27 -0
- package/README.md +2 -2
- package/dist/ai/index.js +17 -0
- package/dist/backend/engine.js +2 -0
- package/dist/backend/env.js +1 -0
- package/dist/backend/index.js +12 -0
- package/dist/backend/stack.js +1 -0
- package/dist/backend/test.js +2 -0
- package/dist/chunks/5W6XJM46.js +13 -0
- package/dist/chunks/6OPBXB7Z.js +2 -0
- package/dist/chunks/7LOMYBLN.js +20 -0
- package/dist/chunks/AN5LCF5K.js +55 -0
- package/dist/chunks/ECUYQ73P.js +2 -0
- package/dist/chunks/HDX6YUAB.js +2 -0
- package/dist/chunks/HVC2C35F.js +2 -0
- package/dist/chunks/I3JNX6MZ.js +4 -0
- package/dist/chunks/JEJ5CFTK.js +2 -0
- package/dist/chunks/K3C2IEDS.js +2 -0
- package/dist/chunks/KRBVYYME.js +2 -0
- package/dist/chunks/NIJHTHNF.js +2 -0
- package/dist/chunks/TL4H6V3P.js +2 -0
- package/dist/chunks/UTRQWCMD.js +6 -0
- package/dist/chunks/ZFPDYRRY.js +2 -0
- package/dist/cli.js +815 -0
- package/dist/refuse/ai.js +2 -0
- package/dist/refuse/backend.js +2 -0
- package/dist/refuse/server.js +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/testing.js +2 -0
- package/dist/web/index.js +2 -0
- package/dist/web/palbe.js +2 -0
- package/dist/web/paywall.js +2 -0
- package/dist/web/vite.js +3 -0
- package/package.json +111 -4
- package/types/ai/src/ai.d.ts +178 -0
- package/types/ai/src/index.d.ts +5 -0
- package/types/ai/src/partial.d.ts +1 -0
- package/types/ai/src/schema.d.ts +47 -0
- package/types/backend/src/bootstrap.d.ts +72 -0
- package/types/backend/src/cache.d.ts +32 -0
- package/types/backend/src/container.d.ts +49 -0
- package/types/backend/src/database.d.ts +148 -0
- package/types/backend/src/db-env.d.ts +4 -0
- package/types/backend/src/decorators.d.ts +293 -0
- package/types/backend/src/engine-db.d.ts +24 -0
- package/types/backend/src/engine-exports.d.ts +3 -0
- package/types/backend/src/engine.d.ts +124 -0
- package/types/backend/src/env.d.ts +27 -0
- package/types/backend/src/errors.d.ts +70 -0
- package/types/backend/src/i18n.d.ts +21 -0
- package/types/backend/src/index.d.ts +23 -0
- package/types/backend/src/openapi.d.ts +73 -0
- package/types/backend/src/platform-calls.d.ts +41 -0
- package/types/backend/src/requests.d.ts +81 -0
- package/types/backend/src/rooms.d.ts +168 -0
- package/types/backend/src/router.d.ts +22 -0
- package/types/backend/src/runtime.d.ts +56 -0
- package/types/backend/src/services.d.ts +262 -0
- package/types/backend/src/stack.d.ts +20 -0
- package/types/backend/src/strings-files.d.ts +3 -0
- package/types/backend/src/testing/index.d.ts +3 -0
- package/types/backend/src/testing/unit.d.ts +66 -0
- package/types/backend/src/upload.d.ts +125 -0
- package/types/backend/src/webhooks.d.ts +113 -0
- package/types/internal/src/event.d.ts +9 -0
- package/types/internal/src/modules/ai.d.ts +678 -0
- package/types/internal/src/modules/analytics.d.ts +1414 -0
- package/types/internal/src/modules/auth.d.ts +5660 -0
- package/types/internal/src/modules/cache.d.ts +158 -0
- package/types/internal/src/modules/events.d.ts +19 -0
- package/types/internal/src/modules/flags.d.ts +957 -0
- package/types/internal/src/modules/index.d.ts +13 -0
- package/types/internal/src/modules/machines.d.ts +1584 -0
- package/types/internal/src/modules/notify.d.ts +1450 -0
- package/types/internal/src/modules/purchases.d.ts +4113 -0
- package/types/internal/src/modules/realtime.d.ts +326 -0
- package/types/internal/src/modules/secrets.d.ts +275 -0
- package/types/internal/src/modules/storage.d.ts +691 -0
- package/types/internal/src/modules/stores.d.ts +1516 -0
- package/types/internal/src/retry.d.ts +14 -0
- package/types/internal/src/transport.d.ts +106 -0
- package/types/server/src/auth/dpop.d.ts +47 -0
- package/types/server/src/auth/jwt.d.ts +115 -0
- package/types/server/src/index.d.ts +9 -0
- package/types/server/src/pages.d.ts +4 -0
- package/types/server/src/purchases.d.ts +51 -0
- package/types/server/src/secrets-env.d.ts +22 -0
- package/types/server/src/server.d.ts +40 -0
- package/types/server/src/testing.d.ts +91 -0
- package/types/server/src/webhooks.d.ts +72 -0
- package/types/web/src/analytics.d.ts +87 -0
- package/types/web/src/auth.d.ts +155 -0
- package/types/web/src/backend-error.d.ts +38 -0
- package/types/web/src/backend-names.d.ts +25 -0
- package/types/web/src/backends.d.ts +84 -0
- package/types/web/src/client.d.ts +51 -0
- package/types/web/src/index.d.ts +13 -0
- package/types/web/src/palbe.d.ts +2 -0
- package/types/web/src/paywall/icons.d.ts +5 -0
- package/types/web/src/paywall/index.d.ts +5 -0
- package/types/web/src/paywall/placement.d.ts +85 -0
- package/types/web/src/paywall/prices.d.ts +40 -0
- package/types/web/src/paywall/render.d.ts +54 -0
- package/types/web/src/paywall/resolve.d.ts +149 -0
- package/types/web/src/purchases.d.ts +86 -0
- package/types/web/src/rooms.d.ts +61 -0
- package/types/web/src/session.d.ts +91 -0
- package/types/web/src/socket.d.ts +87 -0
- package/types/web/src/vite.d.ts +49 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { BucketTypes, Buckets, PalbaseFlagKey, PalbaseSecretName } from './stack.js';
|
|
2
|
+
/** What every call of these services answers, as v2's did. */
|
|
3
|
+
export interface PalbaseResult<T> {
|
|
4
|
+
data: T | null;
|
|
5
|
+
error: {
|
|
6
|
+
message?: string;
|
|
7
|
+
code?: string;
|
|
8
|
+
} | null;
|
|
9
|
+
status?: number;
|
|
10
|
+
}
|
|
11
|
+
type Answer<T> = Promise<PalbaseResult<T>>;
|
|
12
|
+
/**
|
|
13
|
+
* The project's secrets. A machine is started with them as its environment
|
|
14
|
+
* (production's; a backend on a developer's computer reads development's at
|
|
15
|
+
* boot), so a read is a lookup — and a changed secret starts the machine
|
|
16
|
+
* again with the new value.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Secrets: {
|
|
19
|
+
/** The secret's value, or null when the project holds none by that name. */
|
|
20
|
+
get(name: PalbaseSecretName): Promise<string | null>;
|
|
21
|
+
};
|
|
22
|
+
/** An object as storage keeps it. */
|
|
23
|
+
export interface PalbaseFileObject {
|
|
24
|
+
/** Its path in its bucket; `name` is the same. */
|
|
25
|
+
path: string;
|
|
26
|
+
name: string;
|
|
27
|
+
bucket: string;
|
|
28
|
+
size: number;
|
|
29
|
+
contentType: string;
|
|
30
|
+
/** The bytes' SHA-256, hex. */
|
|
31
|
+
checksum: string;
|
|
32
|
+
/** A picture's dimensions. */
|
|
33
|
+
width?: number;
|
|
34
|
+
height?: number;
|
|
35
|
+
/** A picture's placeholder (ThumbHash, base64). */
|
|
36
|
+
thumbhash?: string;
|
|
37
|
+
/** Each rendition the bucket made of it, by name: its public URL. */
|
|
38
|
+
variants: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
export interface PalbaseSignedUrlResponse {
|
|
41
|
+
/** The URL, at the project's own address. */
|
|
42
|
+
signedUrl: string;
|
|
43
|
+
/** When it stops working (RFC 3339). */
|
|
44
|
+
expiresAt: string;
|
|
45
|
+
}
|
|
46
|
+
export interface PalbaseUploadOptions {
|
|
47
|
+
/** Used when the bytes cannot tell (storage sniffs them). */
|
|
48
|
+
contentType?: string;
|
|
49
|
+
/** Replace an object already at the path; without it that upload is refused (409 already_exists). */
|
|
50
|
+
upsert?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface PalbaseListOptions {
|
|
53
|
+
/** Objects to answer (100 when absent). */
|
|
54
|
+
limit?: number;
|
|
55
|
+
/** Objects to skip first. */
|
|
56
|
+
offset?: number;
|
|
57
|
+
}
|
|
58
|
+
/** One bucket's objects. */
|
|
59
|
+
export interface PalbaseBucketClient {
|
|
60
|
+
upload(path: string, file: Blob | ArrayBuffer | ReadableStream, options?: PalbaseUploadOptions): Answer<PalbaseFileObject>;
|
|
61
|
+
download(path: string): Answer<Blob>;
|
|
62
|
+
/** The object's public URL, or one rendition's: a public bucket's objects are served there. */
|
|
63
|
+
getPublicUrl(path: string, options?: {
|
|
64
|
+
variant?: string;
|
|
65
|
+
}): string;
|
|
66
|
+
/** A URL that reads the object until it expires — a duration ("90s", "15m", "1h", "7d"), a week at most. */
|
|
67
|
+
createSignedUrl(path: string, options: {
|
|
68
|
+
expiresIn: string;
|
|
69
|
+
}): Answer<PalbaseSignedUrlResponse>;
|
|
70
|
+
/** Objects whose path starts with `prefix`, newest first. */
|
|
71
|
+
list(prefix?: string, options?: PalbaseListOptions): Answer<PalbaseFileObject[]>;
|
|
72
|
+
/** Deletes each, in order; the first refusal ends it. */
|
|
73
|
+
remove(paths: string[]): Answer<PalbaseFileObject[]>;
|
|
74
|
+
move(from: string, to: string): Answer<void>;
|
|
75
|
+
copy(from: string, to: string): Answer<void>;
|
|
76
|
+
}
|
|
77
|
+
/** A bucket the project holds, its renditions in the type: a variant it does not make is a compile error. */
|
|
78
|
+
export interface TypedBucketClient<B extends BucketTypes> extends Omit<PalbaseBucketClient, 'getPublicUrl'> {
|
|
79
|
+
getPublicUrl(path: string, options?: {
|
|
80
|
+
variant?: B['variants'];
|
|
81
|
+
}): string;
|
|
82
|
+
}
|
|
83
|
+
export type EnvBuckets = {
|
|
84
|
+
[K in keyof Buckets]: TypedBucketClient<Buckets[K] extends BucketTypes ? Buckets[K] : BucketTypes>;
|
|
85
|
+
};
|
|
86
|
+
/** Seconds of a duration like "90s", "15m", "1h30m" or "7d", or undefined when it is not one. */
|
|
87
|
+
export declare function durationSeconds(text: string): number | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* The project's buckets: `Storage.buckets.<name>` with the names the project
|
|
90
|
+
* holds in its type, `Storage.bucket(name)` for a name made at run time.
|
|
91
|
+
*/
|
|
92
|
+
/** What Storage is: a bucket by name — what a test's stand-in gives (withServices({ Storage })). */
|
|
93
|
+
export interface StorageService {
|
|
94
|
+
bucket(name: string): PalbaseBucketClient;
|
|
95
|
+
}
|
|
96
|
+
export declare const Storage: StorageService & {
|
|
97
|
+
buckets: EnvBuckets;
|
|
98
|
+
};
|
|
99
|
+
/** A user's id, several, or a topic's followers. */
|
|
100
|
+
export type PalbasePushRecipient = string | string[] | {
|
|
101
|
+
topic: string;
|
|
102
|
+
};
|
|
103
|
+
/** A text, or one per locale. */
|
|
104
|
+
export type PalbaseLocalizedText = string | Record<string, string>;
|
|
105
|
+
export interface PalbasePushSendParams {
|
|
106
|
+
to: PalbasePushRecipient;
|
|
107
|
+
title?: PalbaseLocalizedText;
|
|
108
|
+
body?: PalbaseLocalizedText;
|
|
109
|
+
variables?: Record<string, string>;
|
|
110
|
+
default_locale?: string;
|
|
111
|
+
data?: Record<string, string>;
|
|
112
|
+
image?: string;
|
|
113
|
+
badge?: number;
|
|
114
|
+
sound?: string;
|
|
115
|
+
deep_link?: string;
|
|
116
|
+
collapse_key?: string;
|
|
117
|
+
priority?: 'high' | 'normal';
|
|
118
|
+
/** Seconds, up to 28 days. */
|
|
119
|
+
ttl?: number;
|
|
120
|
+
silent?: boolean;
|
|
121
|
+
content_available?: boolean;
|
|
122
|
+
/** Transactional (the default) reaches every user; any other only those who did not turn it off. */
|
|
123
|
+
category?: string;
|
|
124
|
+
metadata?: Record<string, unknown> | null;
|
|
125
|
+
/** An alert the app's notification service extension rewrites before it shows. */
|
|
126
|
+
mutable_content?: boolean;
|
|
127
|
+
/** The app's localized placeholder banner key, shown until the extension rewrites it. */
|
|
128
|
+
alert_loc_key?: string;
|
|
129
|
+
/** voip: a PushKit call ring, to iOS VoIP devices only. */
|
|
130
|
+
push_type?: 'voip';
|
|
131
|
+
}
|
|
132
|
+
export interface PalbasePushSendResponse {
|
|
133
|
+
/** One per user it was queued for (a topic's is one). */
|
|
134
|
+
message_ids: string[];
|
|
135
|
+
/** The users it was queued for. */
|
|
136
|
+
recipients: number;
|
|
137
|
+
/** Users whose preferences turned this category off. */
|
|
138
|
+
skipped_user_ids: string[];
|
|
139
|
+
}
|
|
140
|
+
export interface PalbaseEmailSendParams {
|
|
141
|
+
to: string | string[];
|
|
142
|
+
subject?: string;
|
|
143
|
+
/** An email template's slug: it is rendered with `variables`, in `locale`. */
|
|
144
|
+
templateSlug?: string;
|
|
145
|
+
locale?: string;
|
|
146
|
+
variables?: Record<string, unknown>;
|
|
147
|
+
html?: string;
|
|
148
|
+
text?: string;
|
|
149
|
+
/** Own provider only. */
|
|
150
|
+
from?: {
|
|
151
|
+
email: string;
|
|
152
|
+
name?: string;
|
|
153
|
+
};
|
|
154
|
+
/** Own provider only. */
|
|
155
|
+
reply_to?: string;
|
|
156
|
+
category?: string;
|
|
157
|
+
}
|
|
158
|
+
export interface PalbaseEmailSendResponse {
|
|
159
|
+
/** The message's, when `to` was one address. */
|
|
160
|
+
message_id?: string;
|
|
161
|
+
/** Each address's, in the list's order, when `to` was a list: null where its preferences turned the category off. */
|
|
162
|
+
message_ids?: (string | null)[];
|
|
163
|
+
/** True when the recipient's preferences turned this category off and nothing was sent. */
|
|
164
|
+
skipped?: boolean;
|
|
165
|
+
}
|
|
166
|
+
/** Push to users or a topic, and email. */
|
|
167
|
+
export declare const Notifications: {
|
|
168
|
+
push: {
|
|
169
|
+
send(params: PalbasePushSendParams): Answer<PalbasePushSendResponse>;
|
|
170
|
+
};
|
|
171
|
+
email: {
|
|
172
|
+
/** One message per address, in order; the first refusal ends it. */
|
|
173
|
+
send(params: PalbaseEmailSendParams): Answer<PalbaseEmailSendResponse>;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/** Any JSON value a flag can hold. */
|
|
177
|
+
export type PalbaseFlagValue = boolean | number | string | null | PalbaseFlagValue[] | {
|
|
178
|
+
[key: string]: PalbaseFlagValue;
|
|
179
|
+
};
|
|
180
|
+
/** Whose values are read: a user's, by id (null: nobody's — the flags' own). */
|
|
181
|
+
export interface PalbaseFlagContext {
|
|
182
|
+
userId?: string | null;
|
|
183
|
+
/**
|
|
184
|
+
* v2's; read by no flag there or here. v3's conditions read what a client
|
|
185
|
+
* declares (platform, versions, locale) and the user, and nothing given
|
|
186
|
+
* here: a context with only properties reads the request's user's values.
|
|
187
|
+
*/
|
|
188
|
+
properties?: Record<string, unknown>;
|
|
189
|
+
}
|
|
190
|
+
export interface PalbaseFlagVariant {
|
|
191
|
+
name: string;
|
|
192
|
+
}
|
|
193
|
+
export interface PalbaseFlag {
|
|
194
|
+
name: string;
|
|
195
|
+
enabled: boolean;
|
|
196
|
+
variant?: PalbaseFlagVariant;
|
|
197
|
+
}
|
|
198
|
+
/** user: the user's own value; system: the flag's, resolved for them. */
|
|
199
|
+
export type PalbaseFlagSource = 'system' | 'user';
|
|
200
|
+
export interface PalbaseSetOverrideResult {
|
|
201
|
+
key: string;
|
|
202
|
+
value: PalbaseFlagValue;
|
|
203
|
+
source: PalbaseFlagSource;
|
|
204
|
+
}
|
|
205
|
+
/** The user's value once theirs is gone: the flag's own. */
|
|
206
|
+
export type PalbaseClearOverrideResult = PalbaseSetOverrideResult;
|
|
207
|
+
export interface PalbaseSetOverridesResult {
|
|
208
|
+
values: Record<string, PalbaseFlagValue>;
|
|
209
|
+
}
|
|
210
|
+
export interface PalbaseClearAllOverridesResult {
|
|
211
|
+
deleted: number;
|
|
212
|
+
}
|
|
213
|
+
export interface PalbaseBatchOverrideOperation {
|
|
214
|
+
userId: string;
|
|
215
|
+
values: Record<string, PalbaseFlagValue>;
|
|
216
|
+
}
|
|
217
|
+
export interface PalbaseBatchSetOverridesResult {
|
|
218
|
+
applied: number;
|
|
219
|
+
}
|
|
220
|
+
/** Any user's values, by id — the writes of a service, not of the user it serves. */
|
|
221
|
+
export interface PalbaseFlagsServiceClient {
|
|
222
|
+
setOverrideForUser(userId: string, key: PalbaseFlagKey, value: PalbaseFlagValue): Answer<PalbaseSetOverrideResult>;
|
|
223
|
+
setOverridesForUser(userId: string, values: Record<string, PalbaseFlagValue>): Answer<PalbaseSetOverridesResult>;
|
|
224
|
+
clearOverrideForUser(userId: string, key: PalbaseFlagKey): Answer<PalbaseClearOverrideResult>;
|
|
225
|
+
clearAllOverridesForUser(userId: string): Answer<PalbaseClearAllOverridesResult>;
|
|
226
|
+
/** 1000 operations at most. */
|
|
227
|
+
batchSetOverrides(operations: ReadonlyArray<PalbaseBatchOverrideOperation>): Answer<PalbaseBatchSetOverridesResult>;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Feature flags, read for the request's user (a context names another, or
|
|
231
|
+
* nobody), as v2's: a reader's values are theirs first, then the flag's.
|
|
232
|
+
* `setOverride` writes the request's user's own value; any other user's is
|
|
233
|
+
* `$asService()`'s.
|
|
234
|
+
*/
|
|
235
|
+
export declare const Flags: {
|
|
236
|
+
/** Whether the flag is on: a value that cannot be read is off, and the reason is logged — never thrown. */
|
|
237
|
+
isEnabled(flagName: PalbaseFlagKey, context?: PalbaseFlagContext): Promise<boolean>;
|
|
238
|
+
/** A string value's variant; null for any other. */
|
|
239
|
+
getVariant(flagName: PalbaseFlagKey, context?: PalbaseFlagContext): Answer<PalbaseFlagVariant>;
|
|
240
|
+
getAll(context?: PalbaseFlagContext): Answer<PalbaseFlag[]>;
|
|
241
|
+
/**
|
|
242
|
+
* The flag's value: `get(key)`, `get(key, default)`, `get(key, context)`,
|
|
243
|
+
* `get(key, default, context)`. The default stands in for a flag that is
|
|
244
|
+
* not there, and for values that could not be read.
|
|
245
|
+
*/
|
|
246
|
+
get(flagName: PalbaseFlagKey, defaultOrContext?: PalbaseFlagValue | PalbaseFlagContext, maybeContext?: PalbaseFlagContext): Answer<PalbaseFlagValue>;
|
|
247
|
+
/** The request's user's own value of the flag; a request with no user is refused (400). */
|
|
248
|
+
setOverride(key: PalbaseFlagKey, value: PalbaseFlagValue): Answer<PalbaseSetOverrideResult>;
|
|
249
|
+
/** Any user's values, by id. */
|
|
250
|
+
$asService(): PalbaseFlagsServiceClient;
|
|
251
|
+
};
|
|
252
|
+
/** Events to a channel's subscribers, and its state every joiner is handed. */
|
|
253
|
+
export declare const Realtime: {
|
|
254
|
+
/** Everyone subscribed to `channel` gets `event` with `payload`; the answer is the platform's taking it, not their receiving it. */
|
|
255
|
+
broadcast(channel: string, event: string, payload?: Record<string, unknown>): Answer<void>;
|
|
256
|
+
state: {
|
|
257
|
+
/** One entry of the channel's state: subscribers have it, and so does whoever joins later. */
|
|
258
|
+
set(topic: string, key: string, value: Record<string, unknown>): Answer<void>;
|
|
259
|
+
clear(topic: string, key: string): Answer<void>;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The project's secrets, by name. */
|
|
2
|
+
export interface Secrets {
|
|
3
|
+
}
|
|
4
|
+
/** The project's flags, by key. */
|
|
5
|
+
export interface Flags {
|
|
6
|
+
}
|
|
7
|
+
/** One bucket: the renditions it declares (none: `never`, and no variant is asked for). */
|
|
8
|
+
export interface BucketTypes {
|
|
9
|
+
variants: string;
|
|
10
|
+
}
|
|
11
|
+
/** The project's buckets, by name, each with its renditions. */
|
|
12
|
+
export interface Buckets {
|
|
13
|
+
}
|
|
14
|
+
/** The project's roles, by name. */
|
|
15
|
+
export interface Roles {
|
|
16
|
+
}
|
|
17
|
+
export type PalbaseSecretName = keyof Secrets & string;
|
|
18
|
+
export type PalbaseFlagKey = keyof Flags & string;
|
|
19
|
+
export type PalbaseBucketName = keyof Buckets & string;
|
|
20
|
+
export type PalbaseRoleName = keyof Roles & string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { fakeDatabase, isolated, withServices, type FakeDatabase, type IsolatedContainer, type RecordedQuery, type RuntimeServices, type WithServicesOptions } from './unit.js';
|
|
2
|
+
export { loadStringsTable } from '../strings-files.js';
|
|
3
|
+
export { api, createTestApi, testRun, TestApiError, type CallOptions, type ErrorEnvelope, type RecordedRequest, type TestApi, type TestApiConfig, type TestIdentity, type TestRun } from '../../../server/src/testing.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import type { DBClient, EnvTypedDatabase } from 'palgres';
|
|
3
|
+
import { type FakeClient, type RecordedQuery } from 'palgres/testing';
|
|
4
|
+
import type { CacheClient } from '../cache.js';
|
|
5
|
+
import type { Schemas, Tables } from '../db-env.js';
|
|
6
|
+
import type { Token } from '../decorators.js';
|
|
7
|
+
import { type Logger } from '../runtime.js';
|
|
8
|
+
import type { Flags, Notifications, Realtime, Secrets, StorageService } from '../services.js';
|
|
9
|
+
import { type StringsTable } from '../i18n.js';
|
|
10
|
+
export type { RecordedQuery };
|
|
11
|
+
/** What a test can stand in for: the raw database client (fake.raw) and the platform's services. */
|
|
12
|
+
export interface RuntimeServices {
|
|
13
|
+
Database: DBClient;
|
|
14
|
+
Log: Logger;
|
|
15
|
+
Cache: CacheClient;
|
|
16
|
+
Secrets: typeof Secrets;
|
|
17
|
+
Storage: StorageService;
|
|
18
|
+
Notifications: typeof Notifications;
|
|
19
|
+
Flags: typeof Flags;
|
|
20
|
+
Realtime: typeof Realtime;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `fn` with these stand-ins: Database is the client given (its typed
|
|
24
|
+
* surface built over it, as a request's is), each service the object given.
|
|
25
|
+
* A service not given is refused by name when read (its real calls would
|
|
26
|
+
* reach a platform no test serves) — Log alone writes as it always does —
|
|
27
|
+
* and Database not given is none.
|
|
28
|
+
*/
|
|
29
|
+
export interface WithServicesOptions {
|
|
30
|
+
/** The app's strings (loadStringsTable), for t() to answer from. */
|
|
31
|
+
strings?: StringsTable;
|
|
32
|
+
/** The Accept-Language the code runs for: the languages t() answers in, best first, the source last. */
|
|
33
|
+
acceptLanguage?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function withServices<T>(services: Partial<RuntimeServices>, fn: () => T, options?: WithServicesOptions): T;
|
|
36
|
+
export interface IsolatedContainer {
|
|
37
|
+
/** `token` is `value` wherever it is asked for, however deep. */
|
|
38
|
+
with<T>(token: Token<T>, value: T): IsolatedContainer;
|
|
39
|
+
/** `token` built, its constructor's dependencies too, each once in this container. */
|
|
40
|
+
get<T>(token: Token<T>): T;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A container of its own: what get() builds is built from the constructor's
|
|
44
|
+
* declared parameter types, the overrides given with with() in their place.
|
|
45
|
+
* Nothing is shared with the app's container or another isolated().
|
|
46
|
+
*/
|
|
47
|
+
export declare function isolated(): IsolatedContainer;
|
|
48
|
+
export interface FakeDatabase {
|
|
49
|
+
/** The typed Database over the store, as a service reads it. */
|
|
50
|
+
db: EnvTypedDatabase<Tables, Schemas>;
|
|
51
|
+
/** The client itself: what withServices({ Database }) takes. */
|
|
52
|
+
raw: FakeClient;
|
|
53
|
+
/** Every raw SQL call made through it (recorded, not run). */
|
|
54
|
+
queries: readonly RecordedQuery[];
|
|
55
|
+
seed(table: string, rows: Record<string, unknown>[]): void;
|
|
56
|
+
inserted(table: string): Record<string, unknown>[];
|
|
57
|
+
updated(table: string): Record<string, unknown>[];
|
|
58
|
+
deleted(table: string): string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A database in memory (palgres/testing): the engine's operations, filters and refusals, no SQL run.
|
|
62
|
+
* It has no schema (no column default, type or constraint), reads text of digits as a number, and
|
|
63
|
+
* gives rows in the order they were written without orderBy — palgres/testing says how each differs
|
|
64
|
+
* from Postgres.
|
|
65
|
+
*/
|
|
66
|
+
export declare function fakeDatabase(): FakeDatabase;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type RouteOptions } from './decorators.js';
|
|
2
|
+
import type { Route } from './router.js';
|
|
3
|
+
import { type SignedUser } from './platform-calls.js';
|
|
4
|
+
/** Where an @Upload route's files go. */
|
|
5
|
+
export interface UploadConfig {
|
|
6
|
+
/**
|
|
7
|
+
* The bucket's name: lowercase letters, digits and dashes, starting with a
|
|
8
|
+
* letter, 2 to 63 long. Its rules — the largest file, the types it takes,
|
|
9
|
+
* its renditions — are the bucket's, and storage holds every upload to them
|
|
10
|
+
* as it writes: the one check a client cannot step around.
|
|
11
|
+
*/
|
|
12
|
+
bucket: string;
|
|
13
|
+
/**
|
|
14
|
+
* The object's path, made here for each upload — the client never chooses
|
|
15
|
+
* it. `{userId}` is the signed-in user's id ("anonymous" for nobody),
|
|
16
|
+
* `{uploadId}` the upload's own id (storage's), `{filename}` the name the
|
|
17
|
+
* client gave its file, made one path segment (no slash, no leading dot, no
|
|
18
|
+
* control character; "file" when there is none). e.g.
|
|
19
|
+
* "{userId}/{uploadId}-{filename}".
|
|
20
|
+
*/
|
|
21
|
+
pathTemplate: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The stored file, as an @Upload handler is given it (@UploadedObject()): what
|
|
25
|
+
* storage made of the bytes, never the bytes.
|
|
26
|
+
*/
|
|
27
|
+
export interface UploadedObject {
|
|
28
|
+
/** The upload's id: the same in the path ({uploadId}) and here. */
|
|
29
|
+
uploadId: string;
|
|
30
|
+
/** The object's path in its bucket, as pathTemplate made it. */
|
|
31
|
+
path: string;
|
|
32
|
+
bucket: string;
|
|
33
|
+
/** Bytes, as storage counted them. */
|
|
34
|
+
size: number;
|
|
35
|
+
/** The type storage found in the bytes — not the file's name or what the client claimed. */
|
|
36
|
+
contentType: string;
|
|
37
|
+
/** SHA-256 of the stored bytes, hex. */
|
|
38
|
+
checksum: string;
|
|
39
|
+
/** A picture's width in pixels; absent for anything that is not one. */
|
|
40
|
+
width?: number;
|
|
41
|
+
/** A picture's height in pixels. */
|
|
42
|
+
height?: number;
|
|
43
|
+
/** A picture's placeholder (~25 bytes) a client paints while the picture loads; absent otherwise. */
|
|
44
|
+
thumbhash?: string;
|
|
45
|
+
/** The renditions the bucket declares, by name, each its address — a public bucket's; none for a private one. */
|
|
46
|
+
variants: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A route a client sends a file to, through storage: its method is the
|
|
50
|
+
* handler that runs once the file is stored, and its decorator says where the
|
|
51
|
+
* file goes. The handler takes the stored object with @UploadedObject(), the
|
|
52
|
+
* client's body with @Body(schema), its caller with @User() / @OptionalUser(),
|
|
53
|
+
* the path's @Param and the query's @QueryParams, as any route does; what it
|
|
54
|
+
* answers — 200 with JSON, 204, a Response it built, a refusal — is what the
|
|
55
|
+
* client is answered, and a refusal deletes the object.
|
|
56
|
+
*
|
|
57
|
+
* The caller is the one storage names: its id and session, not its token —
|
|
58
|
+
* so a route of this kind cannot ask for a verified email, and its handler
|
|
59
|
+
* reads no header of the client's (@Headers, @Req and @Client are refused).
|
|
60
|
+
*
|
|
61
|
+
* @Upload('/avatar', { bucket: 'avatars', pathTemplate: '{userId}/{uploadId}-{filename}' })
|
|
62
|
+
* setAvatar(@UploadedObject() file: UploadedObject, @User() user: UserT) { … }
|
|
63
|
+
*/
|
|
64
|
+
export declare function Upload(subpath: string, config: UploadConfig & Pick<RouteOptions, 'auth' | 'rateLimit'>): MethodDecorator;
|
|
65
|
+
/**
|
|
66
|
+
* The stored object (: UploadedObject), in an @Upload route's handler — its
|
|
67
|
+
* path, size, type, checksum, a picture's size and placeholder, its
|
|
68
|
+
* renditions.
|
|
69
|
+
*/
|
|
70
|
+
export declare function UploadedObject(): ParameterDecorator;
|
|
71
|
+
/**
|
|
72
|
+
* pathTemplate filled for one upload. {filename} is the one token the client
|
|
73
|
+
* chooses, and each token is made one segment: without that,
|
|
74
|
+
* "../../../etc/passwd" is a file name, and a template that looks like a
|
|
75
|
+
* folder per user becomes a way to write in anyone's.
|
|
76
|
+
*/
|
|
77
|
+
export declare function renderPath(template: string, tokens: {
|
|
78
|
+
userId?: string | null;
|
|
79
|
+
uploadId: string;
|
|
80
|
+
filename?: string;
|
|
81
|
+
}): string;
|
|
82
|
+
/**
|
|
83
|
+
* A value made something one path segment can hold, as v2 made it: control
|
|
84
|
+
* characters gone, a slash or backslash a dash, a run of dots one dot, no dot
|
|
85
|
+
* or space in front (so never "." or ".."), "file" for nothing, 200
|
|
86
|
+
* characters at most.
|
|
87
|
+
*/
|
|
88
|
+
export declare function sanitizeSegment(raw: string): string;
|
|
89
|
+
/** The path a rendered template makes, or why storage would not keep it. */
|
|
90
|
+
export declare function renderedPath(template: string, tokens: {
|
|
91
|
+
userId?: string | null;
|
|
92
|
+
uploadId: string;
|
|
93
|
+
filename?: string;
|
|
94
|
+
}): {
|
|
95
|
+
path: string;
|
|
96
|
+
} | {
|
|
97
|
+
problem: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The rules of @Upload a route table is held to, named at boot: an upload's
|
|
101
|
+
* method is that and no other route (a second, ordinary route would run the
|
|
102
|
+
* handler with no object), @UploadedObject is an upload's alone, and an
|
|
103
|
+
* upload's handler reads no header of the client's — storage forwards none.
|
|
104
|
+
*/
|
|
105
|
+
export declare function checkUploadRoutes(table: readonly Route[]): void;
|
|
106
|
+
/** One of storage's calls about an upload, as it sends it. */
|
|
107
|
+
export interface UploadCall {
|
|
108
|
+
/** The route's path as the client's request names it, each parameter percent-encoded: "/posts/a%2Fb/photo". */
|
|
109
|
+
route: string;
|
|
110
|
+
method: string;
|
|
111
|
+
/** The query the client sent, raw ("" for none). */
|
|
112
|
+
query: string;
|
|
113
|
+
uploadId: string;
|
|
114
|
+
user: SignedUser;
|
|
115
|
+
/** The address the edge saw the upload come from, when storage names it: an anonymous caller's limit is counted by it. */
|
|
116
|
+
client?: string;
|
|
117
|
+
/** authorize: the name the client gave its file ("" for none). */
|
|
118
|
+
filename: string;
|
|
119
|
+
/** complete: the stored object. */
|
|
120
|
+
uploadedObject?: UploadedObject;
|
|
121
|
+
/** The body the client sent beside the file — null when it sent none; at authorize, absent when storage did not say it. */
|
|
122
|
+
body?: unknown;
|
|
123
|
+
}
|
|
124
|
+
/** Storage's call of `step`, or what it is missing. */
|
|
125
|
+
export declare function uploadCallOf(step: 'authorize' | 'complete', v: unknown): UploadCall | string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type WebhookDeclaration, type WebhookProvider } from './decorators.js';
|
|
2
|
+
/** What a handler of a @Webhook is given beside the event, as in v2. */
|
|
3
|
+
export interface WebhookMeta {
|
|
4
|
+
/** The app's environment variables, as it started. */
|
|
5
|
+
env: Record<string, string>;
|
|
6
|
+
/** The delivery's request id: the app's log lines of it carry it. */
|
|
7
|
+
requestId: string;
|
|
8
|
+
/** The project the app belongs to (its id). */
|
|
9
|
+
environmentId: string;
|
|
10
|
+
}
|
|
11
|
+
/** A @Webhook's @On method: the event (its JSON, or the body's text when it is not JSON), and the meta. */
|
|
12
|
+
export type WebhookEventHandler = (event: unknown, meta: WebhookMeta) => Promise<unknown>;
|
|
13
|
+
/** What a @Webhook declares — its name, provider, the secret's env var and its events — read without building the class. */
|
|
14
|
+
export declare function getWebhookManifest(ctor: Function): {
|
|
15
|
+
name: string;
|
|
16
|
+
provider: WebhookProvider | undefined;
|
|
17
|
+
secretEnv: string;
|
|
18
|
+
events: string[];
|
|
19
|
+
};
|
|
20
|
+
export type InboundRefusal = 'missing_signature' | 'malformed_signature' | 'stale_timestamp' | 'signature_mismatch';
|
|
21
|
+
export interface Delivery {
|
|
22
|
+
headers: Headers;
|
|
23
|
+
/** The body's bytes as they arrived. */
|
|
24
|
+
body: Uint8Array;
|
|
25
|
+
/** The URL the sender called, as it called it: Twilio signs it. */
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
/** What a delivery carries, once its signature is checked. */
|
|
29
|
+
export interface Verified {
|
|
30
|
+
/** Its event's type, the @On that takes it ("" when it names none). */
|
|
31
|
+
type: string;
|
|
32
|
+
/** What the handler is given: the body's JSON (its text when it is not JSON), Slack's interactivity payload, or the parameters Twilio signed. */
|
|
33
|
+
event: unknown;
|
|
34
|
+
/** The sender's id of the delivery, when it gives one: one delivered again is not run again. */
|
|
35
|
+
id?: string;
|
|
36
|
+
/** A batch's (SendGrid's): each of its events' ids, in order. */
|
|
37
|
+
ids?: (string | undefined)[];
|
|
38
|
+
}
|
|
39
|
+
/** A sender's scheme, made once from its declaration and secret. */
|
|
40
|
+
export interface Scheme {
|
|
41
|
+
/** The header its signature arrives in: a delivery without it is refused before its body is read. */
|
|
42
|
+
header: string;
|
|
43
|
+
/** Bytes a delivery may be: its sender's largest, and no more than the app's body limit. */
|
|
44
|
+
maxBytes: (appLimit: number) => number;
|
|
45
|
+
/** The refusal, or what a delivery it signed carries. */
|
|
46
|
+
check: (delivery: Delivery, nowSeconds: number) => InboundRefusal | Verified;
|
|
47
|
+
}
|
|
48
|
+
/** A @Webhook as the engine serves it. */
|
|
49
|
+
export interface Inbound {
|
|
50
|
+
name: string;
|
|
51
|
+
cls: Function;
|
|
52
|
+
provider?: WebhookProvider;
|
|
53
|
+
signatureHeader?: string;
|
|
54
|
+
secretEnv: string;
|
|
55
|
+
scheme: Scheme;
|
|
56
|
+
events: Map<string, {
|
|
57
|
+
name: string;
|
|
58
|
+
call: WebhookEventHandler;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The app's @Webhook classes as served, by name, each secret read from env
|
|
63
|
+
* now: one unset, or not a key, is named before anything starts — the
|
|
64
|
+
* variable and the webhook, never the value.
|
|
65
|
+
*/
|
|
66
|
+
export declare function inboundOf(classes: Function[], instanceOf: (cls: Function) => object, env: Record<string, string | undefined>, opts?: {
|
|
67
|
+
secrets?: boolean;
|
|
68
|
+
}): Map<string, Inbound>;
|
|
69
|
+
/**
|
|
70
|
+
* The URL the sender called: https, as every machine is reached — the
|
|
71
|
+
* platform ends TLS, so a forwarded scheme is not taken — on the Host the
|
|
72
|
+
* machine's edge keeps, lowercased. The Host is unsigned, but it cannot
|
|
73
|
+
* carry another address's delivery here: the edge routes by it, so that
|
|
74
|
+
* Host reaches that address's machine.
|
|
75
|
+
*/
|
|
76
|
+
export declare function senderUrl(request: Request, url: URL): string;
|
|
77
|
+
/** The scheme of a declaration under its secret; throws when the secret cannot be its key. */
|
|
78
|
+
export declare function schemeOf(d: Pick<WebhookDeclaration, 'provider' | 'signature'>, secret: string): Scheme;
|
|
79
|
+
/**
|
|
80
|
+
* The event's type, the @On that takes it: as v2 read it — Stripe's and
|
|
81
|
+
* Slack's `type` (an interactivity payload's too), GitHub's X-GitHub-Event
|
|
82
|
+
* and the payload's action (pull_request.opened), Discord's interaction type
|
|
83
|
+
* (PING, INTERACTION_CREATE), SendGrid's batch (event_batch), Twilio's
|
|
84
|
+
* EventType parameter, LiveKit's `event`; and a signature spelled out, the
|
|
85
|
+
* body's `type`. An event it is not in gives "", which an @On("") takes.
|
|
86
|
+
*
|
|
87
|
+
* GitHub's X-GitHub-Event is a header its signature does not cover: whoever
|
|
88
|
+
* holds one of its deliveries can send it again under another event's name,
|
|
89
|
+
* to that event's @On. GitHub signs nothing else that names the event, and
|
|
90
|
+
* every receiver routes on it (Octokit's too) — a GitHub handler checks the
|
|
91
|
+
* payload is the one it takes.
|
|
92
|
+
*/
|
|
93
|
+
export declare function eventTypeOf(provider: WebhookProvider | undefined, headers: Headers, event: unknown): string;
|
|
94
|
+
/**
|
|
95
|
+
* What tells a delivery run once from one delivered again — read from what
|
|
96
|
+
* is signed. Stripe's, Discord's and LiveKit's `id`, Slack's `event_id`,
|
|
97
|
+
* each SendGrid event's `sg_event_id` are in the signed body, and those five
|
|
98
|
+
* sign a time too: a captured delivery is not run twice before its five
|
|
99
|
+
* minutes pass, and is refused after.
|
|
100
|
+
*
|
|
101
|
+
* GitHub and Twilio sign neither a time nor an id. Their ids —
|
|
102
|
+
* X-GitHub-Delivery, I-Twilio-Idempotency-Token (Twilio's "to distinguish
|
|
103
|
+
* retry attempts") — are headers whoever resends a delivery writes, and the
|
|
104
|
+
* same signed request can be two events: one payload dispatched twice, a
|
|
105
|
+
* call <Redirect>ed back to its own URL. So a captured delivery of theirs
|
|
106
|
+
* cannot be told from a new one, and is run again; what is told apart is
|
|
107
|
+
* their own retry — the same id over the same signed bytes (GitHub keeps
|
|
108
|
+
* the GUID on a redelivery). The id is taken with the bytes' hash, so a
|
|
109
|
+
* header alone never makes another delivery look run. Twilio's key is made
|
|
110
|
+
* in its check, from the request it verified. A signature spelled out
|
|
111
|
+
* names no id.
|
|
112
|
+
*/
|
|
113
|
+
export declare function idsOf(provider: WebhookProvider | undefined, d: Delivery, event: unknown): Pick<Verified, 'id' | 'ids'>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface WebhookEvent<T = unknown, K extends string = string> {
|
|
2
|
+
/** Unique per event: the same id twice is the same event delivered again. */
|
|
3
|
+
id: string;
|
|
4
|
+
/** Dotted lowercase words, like purchases.entitlement.changed. */
|
|
5
|
+
type: K;
|
|
6
|
+
project_id: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
data: T;
|
|
9
|
+
}
|