@palbase/backend 17.4.0 → 18.0.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/dist/bin/palbase-backend.cjs +1848 -0
- package/dist/bin/palbase-backend.cjs.map +1 -0
- package/dist/bin/palbase-backend.d.cts +1 -0
- package/dist/bin/palbase-backend.d.ts +1 -0
- package/dist/bin/palbase-backend.js +168 -0
- package/dist/bin/palbase-backend.js.map +1 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/chunk-N32VDWKH.js +172 -0
- package/dist/chunk-N32VDWKH.js.map +1 -0
- package/dist/chunk-POYAFBLF.js +189 -0
- package/dist/chunk-POYAFBLF.js.map +1 -0
- package/dist/chunk-QMVK4X3V.js +200 -0
- package/dist/chunk-QMVK4X3V.js.map +1 -0
- package/dist/chunk-SSGAMC26.js +342 -0
- package/dist/chunk-SSGAMC26.js.map +1 -0
- package/dist/chunk-VYH4U7ZQ.js +1138 -0
- package/dist/chunk-VYH4U7ZQ.js.map +1 -0
- package/dist/{chunk-AAN642N5.js → chunk-W5ODXPY3.js} +2 -336
- package/dist/chunk-W5ODXPY3.js.map +1 -0
- package/dist/chunk-YL4C5NRY.js +90 -0
- package/dist/chunk-YL4C5NRY.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +21 -1
- package/dist/db/env.d.ts +21 -1
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -1
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +9 -6
- package/dist/{index-VLrU7rSW.d.ts → endpoint-B0LpZixz.d.cts} +124 -685
- package/dist/{index-BA_oFAz9.d.cts → endpoint-B0LpZixz.d.ts} +124 -685
- package/dist/engine/index.cjs +1797 -0
- package/dist/engine/index.cjs.map +1 -0
- package/dist/engine/index.d.cts +7 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +43 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/index-B46CGNvx.d.cts +839 -0
- package/dist/index-BGSCWlUa.d.cts +674 -0
- package/dist/index-DZDUMth5.d.ts +839 -0
- package/dist/index-g-EzitI-.d.ts +674 -0
- package/dist/index.cjs +1031 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +290 -532
- package/dist/index.d.ts +290 -532
- package/dist/index.js +999 -509
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +6464 -0
- package/dist/openapi/index.cjs.map +1 -0
- package/dist/openapi/index.d.cts +170 -0
- package/dist/openapi/index.d.ts +170 -0
- package/dist/openapi/index.js +6248 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/registry-3BLYv4si.d.ts +338 -0
- package/dist/registry-Cw0YEYCg.d.cts +338 -0
- package/dist/test/index.js +2 -0
- package/dist/test/index.js.map +1 -1
- package/docs/database.md +16 -3
- package/docs/llms-full.txt +16 -3
- package/package.json +43 -13
- package/stager/package.json +4 -0
- package/stager/return_types.js +338 -0
- package/stager/stage.js +78 -0
- package/stager/throw_analysis.js +726 -0
- package/template/AGENTS.md +261 -0
- package/template/config/secrets.ts +24 -0
- package/template/controllers/health.controller.ts +30 -0
- package/template/db/schema.ts +35 -0
- package/template/package.json +18 -0
- package/template/tsconfig.json +30 -0
- package/LICENSE +0 -21
- package/dist/chunk-AAN642N5.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,239 +2,72 @@ import {
|
|
|
2
2
|
EXTENSION_DEPENDENCIES,
|
|
3
3
|
PALBASE_EXTENSIONS,
|
|
4
4
|
PolicyBuilder,
|
|
5
|
-
TxPlanBuilder,
|
|
6
|
-
TxPlanError,
|
|
7
|
-
TxRefError,
|
|
8
5
|
bigint,
|
|
9
6
|
boolean,
|
|
10
|
-
dec,
|
|
11
7
|
defineSchema,
|
|
12
8
|
enumType,
|
|
13
|
-
inc,
|
|
14
9
|
integer,
|
|
15
10
|
isPalbaseExtension,
|
|
16
11
|
jsonb,
|
|
17
12
|
makeTypedDB,
|
|
18
|
-
now,
|
|
19
13
|
numeric,
|
|
20
14
|
policy,
|
|
21
15
|
raw,
|
|
22
|
-
runTxPlan,
|
|
23
16
|
text,
|
|
24
17
|
timestamp,
|
|
25
18
|
uuid
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function makeTypedSurface(raw2) {
|
|
79
|
-
const ops = {
|
|
80
|
-
query: (sql, params) => raw2.query(sql, params),
|
|
81
|
-
insert: (table, data) => raw2.insert(table, data),
|
|
82
|
-
update: (table, id, data) => raw2.update(table, id, data),
|
|
83
|
-
delete: (table, id) => raw2.delete(table, id),
|
|
84
|
-
findById: (table, id) => raw2.findById(table, id),
|
|
85
|
-
findMany: (table, query) => raw2.findMany(table, query)
|
|
86
|
-
};
|
|
87
|
-
return Object.assign(ops, {
|
|
88
|
-
tables: makeTablesAccessor(() => raw2),
|
|
89
|
-
transaction(fn) {
|
|
90
|
-
const builder = new TxPlanBuilder();
|
|
91
|
-
return runTxPlan(raw2, makeTxTablesAccessor(builder), builder, fn);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
function makeTxTablesAccessor(builder) {
|
|
96
|
-
const tablesProxy = new Proxy(
|
|
97
|
-
{},
|
|
98
|
-
{
|
|
99
|
-
get(_t, prop) {
|
|
100
|
-
if (typeof prop !== "string") return void 0;
|
|
101
|
-
return builder.table(prop);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
return tablesProxy;
|
|
106
|
-
}
|
|
107
|
-
var Database = Object.assign(makeTypedSurface(rawDatabase), {
|
|
108
|
-
/**
|
|
109
|
-
* Lazily resolve the runtime's service-role sibling on each call. We do NOT
|
|
110
|
-
* cache it: `rawDatabase.asService()` reads the CURRENT request scope through
|
|
111
|
-
* the runtime proxy, and the per-request runtime injects a service client
|
|
112
|
-
* bound to that request's identity headers — caching would leak one request's
|
|
113
|
-
* sibling into another concurrent request.
|
|
114
|
-
*/
|
|
115
|
-
asService() {
|
|
116
|
-
return makeTypedSurface(rawDatabase.asService());
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
var Documents = makeServiceProxy("Documents");
|
|
120
|
-
var Storage = makeServiceProxy("Storage");
|
|
121
|
-
var Cache = makeServiceProxy("Cache");
|
|
122
|
-
var Log = makeServiceProxy("Log");
|
|
123
|
-
var Notifications = makeServiceProxy("Notifications");
|
|
124
|
-
var Purchases = makeServiceProxy("Purchases");
|
|
125
|
-
var rawFlags = makeServiceProxy("Flags");
|
|
126
|
-
var Flags = Object.assign(
|
|
127
|
-
{
|
|
128
|
-
isEnabled(flagName, context) {
|
|
129
|
-
return rawFlags.isEnabled(flagName, context);
|
|
130
|
-
},
|
|
131
|
-
getVariant(flagName, context) {
|
|
132
|
-
return rawFlags.getVariant(flagName, context);
|
|
133
|
-
},
|
|
134
|
-
getAll(context) {
|
|
135
|
-
return rawFlags.getAll(context);
|
|
136
|
-
},
|
|
137
|
-
setOverride(key, value) {
|
|
138
|
-
return rawFlags.setOverride(key, value);
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
/**
|
|
143
|
-
* Lazily resolve the runtime's cross-user sibling on each call. We do NOT
|
|
144
|
-
* cache it: `rawFlags.asService()` reads the CURRENT request scope through
|
|
145
|
-
* the runtime proxy, so caching would leak one request's sibling into
|
|
146
|
-
* another concurrent request. Mirrors `Database.asService()`.
|
|
147
|
-
*/
|
|
148
|
-
asService() {
|
|
149
|
-
return rawFlags.asService();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
var Realtime = makeServiceProxy("Realtime");
|
|
154
|
-
|
|
155
|
-
// src/errors.ts
|
|
156
|
-
var HttpError = class extends Error {
|
|
157
|
-
status;
|
|
158
|
-
error;
|
|
159
|
-
errorDescription;
|
|
160
|
-
data;
|
|
161
|
-
constructor(status, error, errorDescription, data) {
|
|
162
|
-
super(errorDescription);
|
|
163
|
-
this.name = "HttpError";
|
|
164
|
-
this.status = status;
|
|
165
|
-
this.error = error;
|
|
166
|
-
this.errorDescription = errorDescription;
|
|
167
|
-
if (data !== void 0) {
|
|
168
|
-
this.data = data;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Serialize to the standard Palbase error response format.
|
|
173
|
-
* The `requestId` is injected by the runtime layer from the request context.
|
|
174
|
-
* When called without arguments (e.g. JSON.stringify), request_id is omitted.
|
|
175
|
-
* When `data` is set, it is appended as a strict-superset field.
|
|
176
|
-
*/
|
|
177
|
-
toJSON(requestId) {
|
|
178
|
-
const result = {
|
|
179
|
-
error: this.error,
|
|
180
|
-
error_description: this.errorDescription,
|
|
181
|
-
status: this.status
|
|
182
|
-
};
|
|
183
|
-
if (requestId) {
|
|
184
|
-
result.request_id = requestId;
|
|
185
|
-
}
|
|
186
|
-
if (this.data !== void 0) {
|
|
187
|
-
result.data = this.data;
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
var PalError = class extends HttpError {
|
|
193
|
-
constructor(status, code, description, data) {
|
|
194
|
-
super(status, code, description, data);
|
|
195
|
-
this.name = "PalError";
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
var NamedHttpError = class extends HttpError {
|
|
199
|
-
constructor(status, defaultCode, name, message, code, data) {
|
|
200
|
-
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
201
|
-
this.name = name;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
function defaultMessage(name) {
|
|
205
|
-
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
206
|
-
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
207
|
-
}
|
|
208
|
-
var BadRequest = class extends NamedHttpError {
|
|
209
|
-
constructor(data, message) {
|
|
210
|
-
super(400, "bad_request", "BadRequest", message, void 0, data);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
var Unauthorized = class extends NamedHttpError {
|
|
214
|
-
constructor(message, code, data) {
|
|
215
|
-
super(401, "unauthorized", "Unauthorized", message, code, data);
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
var Forbidden = class extends NamedHttpError {
|
|
219
|
-
constructor(message, code, data) {
|
|
220
|
-
super(403, "forbidden", "Forbidden", message, code, data);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
var NotFound = class extends NamedHttpError {
|
|
224
|
-
constructor(message, code, data) {
|
|
225
|
-
super(404, "not_found", "NotFound", message, code, data);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
var Conflict = class extends NamedHttpError {
|
|
229
|
-
constructor(message, code, data) {
|
|
230
|
-
super(409, "conflict", "Conflict", message, code, data);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
var TooManyRequests = class extends NamedHttpError {
|
|
234
|
-
constructor(data, message) {
|
|
235
|
-
super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
19
|
+
} from "./chunk-SSGAMC26.js";
|
|
20
|
+
import {
|
|
21
|
+
defineError,
|
|
22
|
+
entitlementFor,
|
|
23
|
+
getErrorRegistry,
|
|
24
|
+
recordEntitlement,
|
|
25
|
+
recordSpend,
|
|
26
|
+
spendFor
|
|
27
|
+
} from "./chunk-N32VDWKH.js";
|
|
28
|
+
import {
|
|
29
|
+
Cache,
|
|
30
|
+
Database,
|
|
31
|
+
Documents,
|
|
32
|
+
Flags,
|
|
33
|
+
Log,
|
|
34
|
+
Notifications,
|
|
35
|
+
Purchases,
|
|
36
|
+
Realtime,
|
|
37
|
+
Secrets,
|
|
38
|
+
Storage,
|
|
39
|
+
__getRuntime,
|
|
40
|
+
__requestALS,
|
|
41
|
+
__runWithRuntime,
|
|
42
|
+
__setRuntime
|
|
43
|
+
} from "./chunk-POYAFBLF.js";
|
|
44
|
+
import {
|
|
45
|
+
TxPlanError,
|
|
46
|
+
TxRefError,
|
|
47
|
+
dec,
|
|
48
|
+
inc,
|
|
49
|
+
now
|
|
50
|
+
} from "./chunk-W5ODXPY3.js";
|
|
51
|
+
import {
|
|
52
|
+
Controller,
|
|
53
|
+
__resetRegisteredControllers,
|
|
54
|
+
getRegisteredControllers
|
|
55
|
+
} from "./chunk-YL4C5NRY.js";
|
|
56
|
+
import {
|
|
57
|
+
BadRequest,
|
|
58
|
+
Conflict,
|
|
59
|
+
Forbidden,
|
|
60
|
+
HttpError,
|
|
61
|
+
NotFound,
|
|
62
|
+
PalError,
|
|
63
|
+
TooManyRequests,
|
|
64
|
+
Unauthorized,
|
|
65
|
+
getRoutes,
|
|
66
|
+
recordParam,
|
|
67
|
+
recordRoute,
|
|
68
|
+
recordThrows
|
|
69
|
+
} from "./chunk-QMVK4X3V.js";
|
|
70
|
+
import "./chunk-7D4SUZUM.js";
|
|
238
71
|
|
|
239
72
|
// src/purchases/errors.ts
|
|
240
73
|
function asPalstoreError(err) {
|
|
@@ -262,31 +95,6 @@ function toHttpError(err) {
|
|
|
262
95
|
}
|
|
263
96
|
}
|
|
264
97
|
|
|
265
|
-
// src/purchases/registry.ts
|
|
266
|
-
var ENTITLEMENTS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.entitlements");
|
|
267
|
-
var SPENDS = /* @__PURE__ */ Symbol.for("palbase.backend.purchases.spends");
|
|
268
|
-
function carrierOf(target) {
|
|
269
|
-
return typeof target === "function" ? target : target.constructor ?? target;
|
|
270
|
-
}
|
|
271
|
-
function own(carrier, slot) {
|
|
272
|
-
if (!Object.prototype.hasOwnProperty.call(carrier, slot)) {
|
|
273
|
-
carrier[slot] = {};
|
|
274
|
-
}
|
|
275
|
-
return carrier[slot];
|
|
276
|
-
}
|
|
277
|
-
function recordEntitlement(target, fnName, key) {
|
|
278
|
-
own(carrierOf(target), ENTITLEMENTS)[fnName] = key;
|
|
279
|
-
}
|
|
280
|
-
function recordSpend(target, fnName, meta) {
|
|
281
|
-
own(carrierOf(target), SPENDS)[fnName] = meta;
|
|
282
|
-
}
|
|
283
|
-
function entitlementFor(ctor, fnName) {
|
|
284
|
-
return carrierOf(ctor)[ENTITLEMENTS]?.[fnName];
|
|
285
|
-
}
|
|
286
|
-
function spendFor(ctor, fnName) {
|
|
287
|
-
return carrierOf(ctor)[SPENDS]?.[fnName];
|
|
288
|
-
}
|
|
289
|
-
|
|
290
98
|
// src/purchases/subject.ts
|
|
291
99
|
var subjectByRequest = /* @__PURE__ */ new WeakMap();
|
|
292
100
|
function storeEnv() {
|
|
@@ -393,6 +201,795 @@ function Spend(key) {
|
|
|
393
201
|
};
|
|
394
202
|
}
|
|
395
203
|
|
|
204
|
+
// src/clients/http.ts
|
|
205
|
+
var PalbaseModuleError = class extends Error {
|
|
206
|
+
code;
|
|
207
|
+
status;
|
|
208
|
+
details;
|
|
209
|
+
constructor(code, message, status, details = {}) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = "PalbaseModuleError";
|
|
212
|
+
this.code = code;
|
|
213
|
+
this.status = status;
|
|
214
|
+
this.details = details;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function makeHttpClient(cfg) {
|
|
218
|
+
async function request(method, path, options = {}) {
|
|
219
|
+
const headers = {
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
...options.headers ?? {}
|
|
222
|
+
};
|
|
223
|
+
if (cfg.apiKey) headers["apikey"] = cfg.apiKey;
|
|
224
|
+
const init = { method, headers };
|
|
225
|
+
if (options.body !== void 0) {
|
|
226
|
+
if (typeof options.body === "string" || options.body instanceof Uint8Array) {
|
|
227
|
+
init.body = options.body;
|
|
228
|
+
} else if (typeof FormData !== "undefined" && options.body instanceof FormData) {
|
|
229
|
+
delete headers["Content-Type"];
|
|
230
|
+
init.body = options.body;
|
|
231
|
+
} else if (typeof Blob !== "undefined" && options.body instanceof Blob) {
|
|
232
|
+
delete headers["Content-Type"];
|
|
233
|
+
init.body = options.body;
|
|
234
|
+
} else {
|
|
235
|
+
init.body = JSON.stringify(options.body);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (options.signal) init.signal = options.signal;
|
|
239
|
+
const doFetch = cfg.fetchImpl ?? globalThis.fetch;
|
|
240
|
+
let response;
|
|
241
|
+
try {
|
|
242
|
+
response = await doFetch(`${cfg.baseUrl}${path}`, init);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return {
|
|
245
|
+
data: null,
|
|
246
|
+
error: new PalbaseModuleError(
|
|
247
|
+
"network_error",
|
|
248
|
+
err instanceof Error ? err.message : "Network request failed",
|
|
249
|
+
0
|
|
250
|
+
),
|
|
251
|
+
status: 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
255
|
+
let parsed = null;
|
|
256
|
+
if (method !== "HEAD" && contentType.includes("json")) {
|
|
257
|
+
parsed = await response.json().catch(() => null);
|
|
258
|
+
} else if (method !== "HEAD" && contentType.startsWith("image/")) {
|
|
259
|
+
parsed = new Uint8Array(await response.arrayBuffer());
|
|
260
|
+
} else if (method !== "HEAD" && response.body) {
|
|
261
|
+
parsed = await response.text().catch(() => null);
|
|
262
|
+
}
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
const body = parsed && typeof parsed === "object" ? parsed : {};
|
|
265
|
+
const code = typeof body.error === "string" && body.error ? body.error : "unknown_error";
|
|
266
|
+
const message = typeof body.error_description === "string" && body.error_description || response.statusText || "request failed";
|
|
267
|
+
return {
|
|
268
|
+
data: null,
|
|
269
|
+
error: new PalbaseModuleError(code, message, response.status, body),
|
|
270
|
+
status: response.status
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return { data: parsed, error: null, status: response.status };
|
|
274
|
+
}
|
|
275
|
+
return { baseUrl: cfg.baseUrl, request };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/clients/documents.ts
|
|
279
|
+
var SEGMENT_RE = /^[A-Za-z0-9_-]+$/;
|
|
280
|
+
function validateSegment(segment, label) {
|
|
281
|
+
if (!SEGMENT_RE.test(segment)) {
|
|
282
|
+
throw new Error(`Invalid ${label}: "${segment}". Must match ${SEGMENT_RE.source}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function buildDocumentRef(http, path) {
|
|
286
|
+
return {
|
|
287
|
+
path,
|
|
288
|
+
async set(data) {
|
|
289
|
+
return http.request("PUT", `/v1/docs/${path}`, { body: data });
|
|
290
|
+
},
|
|
291
|
+
async get() {
|
|
292
|
+
const response = await http.request("GET", `/v1/docs/${path}`);
|
|
293
|
+
if (response.error) return { data: null, error: response.error, status: response.status };
|
|
294
|
+
const raw2 = response.data ?? {};
|
|
295
|
+
const exists = raw2.exists !== void 0 ? raw2.exists : Boolean(raw2.data || raw2.id);
|
|
296
|
+
const segments = path.split("/");
|
|
297
|
+
const id = raw2.id || segments[segments.length - 1] || "";
|
|
298
|
+
return {
|
|
299
|
+
data: { id, exists, data: () => raw2.data, ref: { path } },
|
|
300
|
+
error: null,
|
|
301
|
+
status: response.status
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
async update(data) {
|
|
305
|
+
return http.request("PATCH", `/v1/docs/${path}`, { body: data });
|
|
306
|
+
},
|
|
307
|
+
async delete() {
|
|
308
|
+
return http.request("DELETE", `/v1/docs/${path}`);
|
|
309
|
+
},
|
|
310
|
+
collection(name) {
|
|
311
|
+
validateSegment(name, "subcollection name");
|
|
312
|
+
return buildCollectionRef(http, `${path}/${name}`);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function buildCollectionRef(http, path, state = { where: [], orderBy: [] }) {
|
|
317
|
+
function snapshot(doc) {
|
|
318
|
+
return {
|
|
319
|
+
id: doc.id,
|
|
320
|
+
exists: true,
|
|
321
|
+
data: () => doc.data,
|
|
322
|
+
ref: { path: `${path}/${doc.id}` }
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function querySnapshot(docs) {
|
|
326
|
+
return {
|
|
327
|
+
docs,
|
|
328
|
+
empty: docs.length === 0,
|
|
329
|
+
size: docs.length,
|
|
330
|
+
docChanges: () => docs.map((doc) => ({ type: "added", doc }))
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
path,
|
|
335
|
+
doc(id) {
|
|
336
|
+
validateSegment(id, "document ID");
|
|
337
|
+
return buildDocumentRef(http, `${path}/${id}`);
|
|
338
|
+
},
|
|
339
|
+
async add(data) {
|
|
340
|
+
const resp = await http.request("POST", `/v1/docs/${path}`, { body: data });
|
|
341
|
+
if (resp.error || !resp.data) {
|
|
342
|
+
return { data: null, error: resp.error, status: resp.status };
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
data: buildDocumentRef(http, `${path}/${resp.data.id}`),
|
|
346
|
+
error: null,
|
|
347
|
+
status: resp.status
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
// Each narrowing returns a NEW ref. A builder that mutated in place would
|
|
351
|
+
// leak one caller's filter into a query another caller had already held.
|
|
352
|
+
where(field, op, value) {
|
|
353
|
+
return buildCollectionRef(http, path, {
|
|
354
|
+
...state,
|
|
355
|
+
where: [...state.where, { field, op, value }]
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
orderBy(field, direction = "asc") {
|
|
359
|
+
return buildCollectionRef(http, path, {
|
|
360
|
+
...state,
|
|
361
|
+
orderBy: [...state.orderBy, { field, direction }]
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
limit(n) {
|
|
365
|
+
return buildCollectionRef(http, path, { ...state, limit: n });
|
|
366
|
+
},
|
|
367
|
+
async get() {
|
|
368
|
+
const narrowed = state.where.length > 0 || state.orderBy.length > 0 || state.limit !== void 0;
|
|
369
|
+
const resp = narrowed ? await http.request(
|
|
370
|
+
"POST",
|
|
371
|
+
`/v1/docs/${path}/query`,
|
|
372
|
+
{ body: queryBody(state) }
|
|
373
|
+
) : await http.request(
|
|
374
|
+
"GET",
|
|
375
|
+
`/v1/docs/${path}`
|
|
376
|
+
);
|
|
377
|
+
if (resp.error) return { data: null, error: resp.error, status: resp.status };
|
|
378
|
+
const docs = (resp.data?.documents ?? []).map(snapshot);
|
|
379
|
+
return { data: querySnapshot(docs), error: null, status: resp.status };
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function queryBody(state) {
|
|
384
|
+
const body = {};
|
|
385
|
+
if (state.where.length > 0) {
|
|
386
|
+
body.where = state.where.map((w) => ({ field: w.field, op: w.op, value: w.value }));
|
|
387
|
+
}
|
|
388
|
+
if (state.orderBy.length > 0) {
|
|
389
|
+
body.orderBy = state.orderBy.map((o) => ({ field: o.field, direction: o.direction }));
|
|
390
|
+
}
|
|
391
|
+
if (state.limit !== void 0) body.limit = state.limit;
|
|
392
|
+
return body;
|
|
393
|
+
}
|
|
394
|
+
var MAX_BATCH = 500;
|
|
395
|
+
function buildDocumentsClient(http) {
|
|
396
|
+
return {
|
|
397
|
+
/**
|
|
398
|
+
* `Documents.doc("users/alice")` — segments are collection/documentId PAIRS,
|
|
399
|
+
* so an odd count addresses a COLLECTION and is refused. Accepting it would
|
|
400
|
+
* write a document whose id happens to be a collection's name.
|
|
401
|
+
*/
|
|
402
|
+
doc(path) {
|
|
403
|
+
const segments = String(path).split("/").filter((s) => s.length > 0);
|
|
404
|
+
if (segments.length === 0 || segments.length % 2 !== 0) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Invalid document path: "${path}". Expected collection/documentId pairs, got ${segments.length} segment(s).`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
segments.forEach((s, i) => validateSegment(s, i % 2 === 0 ? "collection name" : "document ID"));
|
|
410
|
+
return buildDocumentRef(http, segments.join("/"));
|
|
411
|
+
},
|
|
412
|
+
collection(name) {
|
|
413
|
+
validateSegment(name, "collection name");
|
|
414
|
+
return buildCollectionRef(http, name);
|
|
415
|
+
},
|
|
416
|
+
async batch(operations) {
|
|
417
|
+
if (operations.length > MAX_BATCH) {
|
|
418
|
+
return {
|
|
419
|
+
data: null,
|
|
420
|
+
error: new PalbaseModuleError(
|
|
421
|
+
"batch_too_large",
|
|
422
|
+
`Batch size ${operations.length} exceeds maximum of ${MAX_BATCH}`,
|
|
423
|
+
400
|
|
424
|
+
),
|
|
425
|
+
status: 400
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (operations.length === 0) return { data: null, error: null, status: 200 };
|
|
429
|
+
return http.request("POST", "/v1/docs/batch", {
|
|
430
|
+
body: operations.map((op) => ({ op: op.op, path: op.ref.path, data: op.data }))
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/clients/flags.ts
|
|
437
|
+
var FLAG_NAME_RE = /^[A-Za-z0-9_.-]+$/;
|
|
438
|
+
function assertFlagName(flagName) {
|
|
439
|
+
if (!FLAG_NAME_RE.test(flagName)) {
|
|
440
|
+
throw new Error(`Invalid flag name: "${flagName}". Flag names must match ${FLAG_NAME_RE.source}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function flagEnabled(value) {
|
|
444
|
+
if (typeof value === "boolean") return value;
|
|
445
|
+
return value != null && value !== 0 && value !== "";
|
|
446
|
+
}
|
|
447
|
+
function flagVariant(value) {
|
|
448
|
+
return typeof value === "string" ? { name: value } : null;
|
|
449
|
+
}
|
|
450
|
+
function isContextObject(v) {
|
|
451
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && ("userId" in v || "properties" in v);
|
|
452
|
+
}
|
|
453
|
+
function buildFlagsClient(http, cfg) {
|
|
454
|
+
function resolveUserId(context) {
|
|
455
|
+
if (context && "userId" in context) return context.userId ?? null;
|
|
456
|
+
return cfg.getCurrentUserId() || void 0;
|
|
457
|
+
}
|
|
458
|
+
function mergedPath(context) {
|
|
459
|
+
const uid = resolveUserId(context);
|
|
460
|
+
return uid ? `/v1/user-flags/users/${encodeURIComponent(uid)}` : "/v1/user-flags";
|
|
461
|
+
}
|
|
462
|
+
function fetchMerged(context) {
|
|
463
|
+
return http.request("GET", mergedPath(context));
|
|
464
|
+
}
|
|
465
|
+
const service = {
|
|
466
|
+
async setOverrideForUser(userId, key, value) {
|
|
467
|
+
return http.request(
|
|
468
|
+
"PUT",
|
|
469
|
+
`/v1/user-flags/users/${encodeURIComponent(userId)}/${encodeURIComponent(key)}`,
|
|
470
|
+
{ body: { value } }
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
async setOverridesForUser(userId, values) {
|
|
474
|
+
return http.request("PUT", `/v1/user-flags/users/${encodeURIComponent(userId)}`, {
|
|
475
|
+
body: { values }
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
async clearOverrideForUser(userId, key) {
|
|
479
|
+
return http.request(
|
|
480
|
+
"DELETE",
|
|
481
|
+
`/v1/user-flags/users/${encodeURIComponent(userId)}/${encodeURIComponent(key)}`
|
|
482
|
+
);
|
|
483
|
+
},
|
|
484
|
+
async clearAllOverridesForUser(userId) {
|
|
485
|
+
return http.request("DELETE", `/v1/user-flags/users/${encodeURIComponent(userId)}`);
|
|
486
|
+
},
|
|
487
|
+
async batchSetOverrides(operations) {
|
|
488
|
+
const ops = (operations ?? []).map((op) => {
|
|
489
|
+
const raw2 = op;
|
|
490
|
+
return { user_id: raw2.userId ?? raw2.user_id, values: raw2.values };
|
|
491
|
+
});
|
|
492
|
+
return http.request("POST", "/v1/user-flags/batch", { body: { operations: ops } });
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
async isEnabled(flagName, context) {
|
|
497
|
+
assertFlagName(flagName);
|
|
498
|
+
const res = await fetchMerged(context);
|
|
499
|
+
if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
|
|
500
|
+
return { data: flagEnabled(res.data.values?.[flagName]), error: null, status: res.status };
|
|
501
|
+
},
|
|
502
|
+
async getVariant(flagName, context) {
|
|
503
|
+
assertFlagName(flagName);
|
|
504
|
+
const res = await fetchMerged(context);
|
|
505
|
+
if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
|
|
506
|
+
return { data: flagVariant(res.data.values?.[flagName]), error: null, status: res.status };
|
|
507
|
+
},
|
|
508
|
+
/**
|
|
509
|
+
* `get(key)` · `get(key, default)` · `get(key, default, ctx)` · `get(key, ctx)`.
|
|
510
|
+
*
|
|
511
|
+
* The default is substituted when the flag is ABSENT or when the flags
|
|
512
|
+
* service is unreachable — a product that hides a feature because a lookup
|
|
513
|
+
* timed out is behaving correctly; one that crashes is not.
|
|
514
|
+
*/
|
|
515
|
+
async get(flagName, defaultOrContext, maybeContext) {
|
|
516
|
+
assertFlagName(flagName);
|
|
517
|
+
let defaultValue;
|
|
518
|
+
let context;
|
|
519
|
+
let hasDefault = false;
|
|
520
|
+
if (maybeContext !== void 0) {
|
|
521
|
+
defaultValue = defaultOrContext;
|
|
522
|
+
hasDefault = true;
|
|
523
|
+
context = maybeContext;
|
|
524
|
+
} else if (isContextObject(defaultOrContext)) {
|
|
525
|
+
context = defaultOrContext;
|
|
526
|
+
} else if (defaultOrContext !== void 0) {
|
|
527
|
+
defaultValue = defaultOrContext;
|
|
528
|
+
hasDefault = true;
|
|
529
|
+
}
|
|
530
|
+
const resp = await fetchMerged(context);
|
|
531
|
+
if (resp.error !== null) {
|
|
532
|
+
if (hasDefault) return { data: defaultValue ?? null, error: null, status: resp.status };
|
|
533
|
+
return { data: null, error: resp.error, status: resp.status };
|
|
534
|
+
}
|
|
535
|
+
const values = resp.data?.values ?? {};
|
|
536
|
+
const value = values[flagName];
|
|
537
|
+
if (value === void 0) {
|
|
538
|
+
return { data: hasDefault ? defaultValue ?? null : null, error: null, status: resp.status };
|
|
539
|
+
}
|
|
540
|
+
return { data: value, error: null, status: resp.status };
|
|
541
|
+
},
|
|
542
|
+
async getAll(context) {
|
|
543
|
+
const res = await fetchMerged(context);
|
|
544
|
+
if (res.error || res.data == null) return { data: null, error: res.error, status: res.status };
|
|
545
|
+
const values = res.data.values ?? {};
|
|
546
|
+
return {
|
|
547
|
+
data: Object.keys(values).map((name) => {
|
|
548
|
+
const value = values[name];
|
|
549
|
+
const flag2 = {
|
|
550
|
+
name,
|
|
551
|
+
enabled: flagEnabled(value)
|
|
552
|
+
};
|
|
553
|
+
if (typeof value === "string") flag2.variant = { name: value };
|
|
554
|
+
return flag2;
|
|
555
|
+
}),
|
|
556
|
+
error: null,
|
|
557
|
+
status: res.status
|
|
558
|
+
};
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* Override a flag for the CURRENT request user.
|
|
562
|
+
*
|
|
563
|
+
* On an anonymous request this does not silently no-op and does not write
|
|
564
|
+
* for nobody: it refuses and names the call that does cross-user writes.
|
|
565
|
+
*/
|
|
566
|
+
async setOverride(key, value) {
|
|
567
|
+
const uid = cfg.getCurrentUserId();
|
|
568
|
+
if (!uid) {
|
|
569
|
+
return {
|
|
570
|
+
data: null,
|
|
571
|
+
error: {
|
|
572
|
+
message: "setOverride requires a signed-in user; use Flags.asService().setOverrideForUser(userId, key, value) for cross-user writes"
|
|
573
|
+
},
|
|
574
|
+
status: 400
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
return http.request(
|
|
578
|
+
"PUT",
|
|
579
|
+
`/v1/user-flags/users/${encodeURIComponent(uid)}/${encodeURIComponent(key)}`,
|
|
580
|
+
{ body: { value } }
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
asService() {
|
|
584
|
+
return service;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/clients/notifications.ts
|
|
590
|
+
function mapEnvelope(res, map) {
|
|
591
|
+
if (res.error !== null || res.data === null || res.data === void 0) {
|
|
592
|
+
return { data: null, error: res.error, status: res.status };
|
|
593
|
+
}
|
|
594
|
+
return { data: map(res.data), error: null, status: res.status };
|
|
595
|
+
}
|
|
596
|
+
function toEmailTemplate(wire) {
|
|
597
|
+
const view = {
|
|
598
|
+
id: wire.id,
|
|
599
|
+
slug: wire.slug,
|
|
600
|
+
locale: wire.locale,
|
|
601
|
+
subject: wire.subject,
|
|
602
|
+
htmlBody: wire.html_body,
|
|
603
|
+
variables: wire.variables ?? [],
|
|
604
|
+
isDefault: wire.is_default,
|
|
605
|
+
createdAt: wire.created_at,
|
|
606
|
+
updatedAt: wire.updated_at
|
|
607
|
+
};
|
|
608
|
+
if (wire.text_body !== void 0) view.textBody = wire.text_body;
|
|
609
|
+
return view;
|
|
610
|
+
}
|
|
611
|
+
function toSmsTemplate(wire) {
|
|
612
|
+
return {
|
|
613
|
+
id: wire.id,
|
|
614
|
+
slug: wire.slug,
|
|
615
|
+
locale: wire.locale,
|
|
616
|
+
body: wire.body,
|
|
617
|
+
variables: wire.variables ?? [],
|
|
618
|
+
isDefault: wire.is_default,
|
|
619
|
+
createdAt: wire.created_at,
|
|
620
|
+
updatedAt: wire.updated_at
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function buildNotificationsClient(http) {
|
|
624
|
+
const push = {
|
|
625
|
+
async send(params) {
|
|
626
|
+
return http.request("POST", "/v1/notifications/push", { body: params });
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const email = {
|
|
630
|
+
async send(params) {
|
|
631
|
+
const { templateSlug, html, text: text2, ...rest } = params;
|
|
632
|
+
const body = { ...rest };
|
|
633
|
+
if (templateSlug !== void 0) body.template_slug = templateSlug;
|
|
634
|
+
if (html !== void 0) body.html_body = html;
|
|
635
|
+
if (text2 !== void 0) body.text_body = text2;
|
|
636
|
+
return http.request("POST", "/v1/notifications/email", { body });
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
const sms = {
|
|
640
|
+
async send(params) {
|
|
641
|
+
const { templateSlug, ...rest } = params;
|
|
642
|
+
const body = templateSlug !== void 0 ? { ...rest, template_slug: templateSlug } : rest;
|
|
643
|
+
return http.request("POST", "/v1/notifications/sms", { body });
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
const verifications = {
|
|
647
|
+
async start(params) {
|
|
648
|
+
return http.request("POST", "/v1/notifications/verifications", { body: params });
|
|
649
|
+
},
|
|
650
|
+
async check(params) {
|
|
651
|
+
return http.request("POST", "/v1/notifications/verifications/check", { body: params });
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
const inbox = {
|
|
655
|
+
async send(params) {
|
|
656
|
+
return http.request("POST", "/v1/notifications/inbox", { body: params });
|
|
657
|
+
},
|
|
658
|
+
async list(options) {
|
|
659
|
+
const opts = options ?? {};
|
|
660
|
+
const params = new URLSearchParams();
|
|
661
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
662
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
663
|
+
if (opts.is_read !== void 0) params.set("is_read", opts.is_read ? "true" : "false");
|
|
664
|
+
if (opts.category) params.set("category", opts.category);
|
|
665
|
+
if (opts.include_archived) params.set("include_archived", "true");
|
|
666
|
+
const query = params.toString();
|
|
667
|
+
return http.request("GET", `/v1/notifications/inbox${query ? `?${query}` : ""}`);
|
|
668
|
+
},
|
|
669
|
+
async unreadCount() {
|
|
670
|
+
return http.request("GET", "/v1/notifications/inbox/unread-count");
|
|
671
|
+
},
|
|
672
|
+
async markRead(id) {
|
|
673
|
+
return http.request("PATCH", `/v1/notifications/inbox/${encodeURIComponent(id)}/read`);
|
|
674
|
+
},
|
|
675
|
+
async markAllRead() {
|
|
676
|
+
return http.request("POST", "/v1/notifications/inbox/read-all");
|
|
677
|
+
},
|
|
678
|
+
async archive(id) {
|
|
679
|
+
return http.request("DELETE", `/v1/notifications/inbox/${encodeURIComponent(id)}`);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
const preferences = {
|
|
683
|
+
async get() {
|
|
684
|
+
return http.request("GET", "/v1/notifications/preferences");
|
|
685
|
+
},
|
|
686
|
+
async update(params) {
|
|
687
|
+
return http.request("PUT", "/v1/notifications/preferences", { body: params });
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const emailTemplates = {
|
|
691
|
+
async list() {
|
|
692
|
+
const resp = await http.request("GET", "/v1/notifications/templates");
|
|
693
|
+
return mapEnvelope(resp, (rows) => (rows ?? []).map(toEmailTemplate));
|
|
694
|
+
},
|
|
695
|
+
async get(id) {
|
|
696
|
+
const resp = await http.request(
|
|
697
|
+
"GET",
|
|
698
|
+
`/v1/notifications/templates/${encodeURIComponent(id)}`
|
|
699
|
+
);
|
|
700
|
+
return mapEnvelope(resp, toEmailTemplate);
|
|
701
|
+
},
|
|
702
|
+
async create(input) {
|
|
703
|
+
const body = {
|
|
704
|
+
slug: input.slug,
|
|
705
|
+
subject: input.subject,
|
|
706
|
+
html_body: input.htmlBody
|
|
707
|
+
};
|
|
708
|
+
if (input.textBody !== void 0) body.text_body = input.textBody;
|
|
709
|
+
if (input.variables !== void 0) body.variables = input.variables;
|
|
710
|
+
const resp = await http.request("POST", "/v1/notifications/templates", { body });
|
|
711
|
+
return mapEnvelope(resp, toEmailTemplate);
|
|
712
|
+
},
|
|
713
|
+
async update(id, input) {
|
|
714
|
+
const body = {};
|
|
715
|
+
if (input.subject !== void 0) body.subject = input.subject;
|
|
716
|
+
if (input.htmlBody !== void 0) body.html_body = input.htmlBody;
|
|
717
|
+
if (input.textBody !== void 0) body.text_body = input.textBody;
|
|
718
|
+
if (input.variables !== void 0) body.variables = input.variables;
|
|
719
|
+
const resp = await http.request(
|
|
720
|
+
"PUT",
|
|
721
|
+
`/v1/notifications/templates/${encodeURIComponent(id)}`,
|
|
722
|
+
{ body }
|
|
723
|
+
);
|
|
724
|
+
return mapEnvelope(resp, toEmailTemplate);
|
|
725
|
+
},
|
|
726
|
+
async delete(id) {
|
|
727
|
+
return http.request("DELETE", `/v1/notifications/templates/${encodeURIComponent(id)}`);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
const smsTemplates = {
|
|
731
|
+
async list() {
|
|
732
|
+
const resp = await http.request("GET", "/v1/notifications/sms-templates");
|
|
733
|
+
return mapEnvelope(resp, (rows) => (rows ?? []).map(toSmsTemplate));
|
|
734
|
+
},
|
|
735
|
+
async get(id) {
|
|
736
|
+
const resp = await http.request(
|
|
737
|
+
"GET",
|
|
738
|
+
`/v1/notifications/sms-templates/${encodeURIComponent(id)}`
|
|
739
|
+
);
|
|
740
|
+
return mapEnvelope(resp, toSmsTemplate);
|
|
741
|
+
},
|
|
742
|
+
async create(input) {
|
|
743
|
+
const body = { slug: input.slug, body: input.body };
|
|
744
|
+
if (input.variables !== void 0) body.variables = input.variables;
|
|
745
|
+
const resp = await http.request("POST", "/v1/notifications/sms-templates", { body });
|
|
746
|
+
return mapEnvelope(resp, toSmsTemplate);
|
|
747
|
+
},
|
|
748
|
+
async update(id, input) {
|
|
749
|
+
const body = {};
|
|
750
|
+
if (input.body !== void 0) body.body = input.body;
|
|
751
|
+
if (input.variables !== void 0) body.variables = input.variables;
|
|
752
|
+
const resp = await http.request(
|
|
753
|
+
"PUT",
|
|
754
|
+
`/v1/notifications/sms-templates/${encodeURIComponent(id)}`,
|
|
755
|
+
{ body }
|
|
756
|
+
);
|
|
757
|
+
return mapEnvelope(resp, toSmsTemplate);
|
|
758
|
+
},
|
|
759
|
+
async delete(id) {
|
|
760
|
+
return http.request("DELETE", `/v1/notifications/sms-templates/${encodeURIComponent(id)}`);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
return {
|
|
764
|
+
push,
|
|
765
|
+
email,
|
|
766
|
+
sms,
|
|
767
|
+
verifications,
|
|
768
|
+
inbox,
|
|
769
|
+
preferences,
|
|
770
|
+
templates: { email: emailTemplates, sms: smsTemplates },
|
|
771
|
+
async registerDevice(params) {
|
|
772
|
+
return http.request("POST", "/v1/notifications/devices", { body: params });
|
|
773
|
+
},
|
|
774
|
+
async unregisterDevice(deviceId) {
|
|
775
|
+
return http.request("DELETE", `/v1/notifications/devices/${encodeURIComponent(deviceId)}`);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/clients/realtime.ts
|
|
781
|
+
import { createHmac } from "crypto";
|
|
782
|
+
function mintToken(secret2) {
|
|
783
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
784
|
+
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
785
|
+
const payload = Buffer.from(
|
|
786
|
+
JSON.stringify({ iss: "backend-runtime", role: "service_role", iat: now2, exp: now2 + 60 })
|
|
787
|
+
).toString("base64url");
|
|
788
|
+
const sig = createHmac("sha256", secret2).update(`${header}.${payload}`).digest("base64url");
|
|
789
|
+
return `${header}.${payload}.${sig}`;
|
|
790
|
+
}
|
|
791
|
+
function buildRealtimeClient(cfg) {
|
|
792
|
+
const url = `${cfg.baseUrl}/realtime/api/broadcast`;
|
|
793
|
+
return {
|
|
794
|
+
async broadcast(channel, event, payload) {
|
|
795
|
+
if (typeof channel !== "string" || channel.length === 0) {
|
|
796
|
+
return {
|
|
797
|
+
data: null,
|
|
798
|
+
error: new PalbaseModuleError("invalid_argument", "channel must be a non-empty string", 400)
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (typeof event !== "string" || event.length === 0) {
|
|
802
|
+
return {
|
|
803
|
+
data: null,
|
|
804
|
+
error: new PalbaseModuleError("invalid_argument", "event must be a non-empty string", 400)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
if (!cfg.apiJwtSecret) {
|
|
808
|
+
return {
|
|
809
|
+
data: null,
|
|
810
|
+
error: new PalbaseModuleError(
|
|
811
|
+
"realtime_unconfigured",
|
|
812
|
+
"Realtime.broadcast is unavailable: realtime is not provisioned for this Environment (PALBASE_REALTIME_API_JWT_SECRET unset).",
|
|
813
|
+
503
|
|
814
|
+
)
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
let token;
|
|
818
|
+
try {
|
|
819
|
+
token = mintToken(cfg.apiJwtSecret);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
return {
|
|
822
|
+
data: null,
|
|
823
|
+
error: new PalbaseModuleError(
|
|
824
|
+
"realtime_token_error",
|
|
825
|
+
err instanceof Error ? err.message : "token mint failed",
|
|
826
|
+
500
|
|
827
|
+
)
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const body = JSON.stringify({
|
|
831
|
+
messages: [{ topic: channel, event, payload: payload ?? {}, private: false }]
|
|
832
|
+
});
|
|
833
|
+
const doFetch = cfg.fetchImpl ?? globalThis.fetch;
|
|
834
|
+
let response;
|
|
835
|
+
try {
|
|
836
|
+
response = await doFetch(url, {
|
|
837
|
+
method: "POST",
|
|
838
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
839
|
+
body
|
|
840
|
+
});
|
|
841
|
+
} catch (err) {
|
|
842
|
+
return {
|
|
843
|
+
data: null,
|
|
844
|
+
error: new PalbaseModuleError(
|
|
845
|
+
"network_error",
|
|
846
|
+
err instanceof Error ? err.message : "broadcast request failed",
|
|
847
|
+
0
|
|
848
|
+
)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
if (response.status === 202 || response.status === 200) {
|
|
852
|
+
return { data: void 0, error: null };
|
|
853
|
+
}
|
|
854
|
+
const detail = await response.text().catch(() => "");
|
|
855
|
+
return {
|
|
856
|
+
data: null,
|
|
857
|
+
error: new PalbaseModuleError(
|
|
858
|
+
"realtime_broadcast_failed",
|
|
859
|
+
`broadcast returned ${response.status}: ${detail.slice(0, 200)}`,
|
|
860
|
+
response.status
|
|
861
|
+
)
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/clients/storage.ts
|
|
868
|
+
var BUCKET_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
869
|
+
var STORAGE_PATH_RE = /^[a-zA-Z0-9_./-]+$/;
|
|
870
|
+
var STORAGE_TRAVERSAL_RE = /(?:^|\/)\.\.(?:\/|$)/;
|
|
871
|
+
function validateStoragePath(p) {
|
|
872
|
+
if (!STORAGE_PATH_RE.test(p) || STORAGE_TRAVERSAL_RE.test(p)) {
|
|
873
|
+
throw new Error(
|
|
874
|
+
`Invalid file path: "${p}". Paths must not contain traversal sequences and must match ${STORAGE_PATH_RE.source}`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function toFileObject(bucket2, row) {
|
|
879
|
+
return {
|
|
880
|
+
name: row.path ?? "",
|
|
881
|
+
path: row.path ?? "",
|
|
882
|
+
bucket: row.bucket ?? bucket2,
|
|
883
|
+
size: typeof row.size === "number" ? row.size : 0,
|
|
884
|
+
contentType: row.contentType ?? "",
|
|
885
|
+
checksum: row.checksum ?? "",
|
|
886
|
+
width: row.width,
|
|
887
|
+
height: row.height,
|
|
888
|
+
thumbhash: row.thumbhash,
|
|
889
|
+
variants: row.variants ?? {}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function mapResponse(res, map) {
|
|
893
|
+
if (res.error || res.data === null || res.data === void 0) {
|
|
894
|
+
return { data: null, error: res.error, status: res.status };
|
|
895
|
+
}
|
|
896
|
+
return { data: map(res.data), error: null, status: res.status };
|
|
897
|
+
}
|
|
898
|
+
function buildBucketClient(http, bucketName) {
|
|
899
|
+
const objectPath = (path) => `/v1/storage/object/${bucketName}/${path}`;
|
|
900
|
+
return {
|
|
901
|
+
async upload(path, file, options) {
|
|
902
|
+
validateStoragePath(path);
|
|
903
|
+
const headers = {
|
|
904
|
+
"Content-Type": options?.contentType ?? "application/octet-stream"
|
|
905
|
+
};
|
|
906
|
+
if (options?.upsert) headers["x-upsert"] = "true";
|
|
907
|
+
const body = file instanceof ArrayBuffer ? new Uint8Array(file) : file;
|
|
908
|
+
const res = await http.request("PUT", objectPath(path), {
|
|
909
|
+
body,
|
|
910
|
+
headers
|
|
911
|
+
});
|
|
912
|
+
return mapResponse(res, (r) => toFileObject(bucketName, r ?? {}));
|
|
913
|
+
},
|
|
914
|
+
async download(path) {
|
|
915
|
+
validateStoragePath(path);
|
|
916
|
+
return http.request("GET", objectPath(path), { headers: { Accept: "*/*" } });
|
|
917
|
+
},
|
|
918
|
+
getPublicUrl(path, options) {
|
|
919
|
+
validateStoragePath(path);
|
|
920
|
+
const variant = options?.variant ? `?variant=${encodeURIComponent(options.variant)}` : "";
|
|
921
|
+
return `/v1/files/${bucketName}/${path}${variant}`;
|
|
922
|
+
},
|
|
923
|
+
async createSignedUrl(path, options) {
|
|
924
|
+
validateStoragePath(path);
|
|
925
|
+
return http.request("POST", `/v1/storage/sign/${bucketName}/${path}`, {
|
|
926
|
+
body: { expiresIn: options.expiresIn }
|
|
927
|
+
});
|
|
928
|
+
},
|
|
929
|
+
async list(prefix, options) {
|
|
930
|
+
if (prefix) validateStoragePath(prefix);
|
|
931
|
+
const params = new URLSearchParams();
|
|
932
|
+
if (prefix) params.set("prefix", prefix);
|
|
933
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
934
|
+
const query = params.toString();
|
|
935
|
+
const res = await http.request(
|
|
936
|
+
"GET",
|
|
937
|
+
`/v1/storage/bucket/${bucketName}${query ? `?${query}` : ""}`
|
|
938
|
+
);
|
|
939
|
+
return mapResponse(res, (body) => (body?.objects ?? []).map((row) => toFileObject(bucketName, row)));
|
|
940
|
+
},
|
|
941
|
+
async remove(paths) {
|
|
942
|
+
for (const p of paths) validateStoragePath(p);
|
|
943
|
+
const removed = [];
|
|
944
|
+
for (const p of paths) {
|
|
945
|
+
const res = await http.request("DELETE", objectPath(p));
|
|
946
|
+
if (res.error) return { data: null, error: res.error, status: res.status };
|
|
947
|
+
removed.push(toFileObject(bucketName, { path: p }));
|
|
948
|
+
}
|
|
949
|
+
return { data: removed, error: null, status: 200 };
|
|
950
|
+
},
|
|
951
|
+
async move(from, to) {
|
|
952
|
+
validateStoragePath(from);
|
|
953
|
+
validateStoragePath(to);
|
|
954
|
+
return http.request("POST", `/v1/storage/move/${bucketName}`, { body: { from, to } });
|
|
955
|
+
},
|
|
956
|
+
async copy(from, to) {
|
|
957
|
+
validateStoragePath(from);
|
|
958
|
+
validateStoragePath(to);
|
|
959
|
+
return http.request("POST", `/v1/storage/copy/${bucketName}`, { body: { from, to } });
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function buildStorageClient(http) {
|
|
964
|
+
return {
|
|
965
|
+
bucket(name) {
|
|
966
|
+
if (!BUCKET_NAME_RE.test(name)) {
|
|
967
|
+
throw new Error(`Invalid bucket name: "${name}". Bucket names must match ${BUCKET_NAME_RE.source}`);
|
|
968
|
+
}
|
|
969
|
+
return buildBucketClient(http, name);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/clients/index.ts
|
|
975
|
+
function buildModuleClients(cfg) {
|
|
976
|
+
const baseUrl = (cfg.baseUrl || "").replace(/\/+$/, "");
|
|
977
|
+
if (!baseUrl) return {};
|
|
978
|
+
const apiKey = cfg.serviceRoleKey || cfg.apiKey || "";
|
|
979
|
+
const http = makeHttpClient({ baseUrl, apiKey, fetchImpl: cfg.fetchImpl });
|
|
980
|
+
return {
|
|
981
|
+
Documents: buildDocumentsClient(http),
|
|
982
|
+
Storage: buildStorageClient(http),
|
|
983
|
+
Notifications: buildNotificationsClient(http),
|
|
984
|
+
Flags: buildFlagsClient(http, { getCurrentUserId: cfg.getCurrentUserId }),
|
|
985
|
+
Realtime: buildRealtimeClient({
|
|
986
|
+
baseUrl,
|
|
987
|
+
apiJwtSecret: cfg.realtimeApiJwtSecret,
|
|
988
|
+
fetchImpl: cfg.fetchImpl
|
|
989
|
+
})
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
396
993
|
// src/purchases/keys-gen.ts
|
|
397
994
|
function liveKeys(entries) {
|
|
398
995
|
return Object.entries(entries ?? {}).filter(([, def]) => def?.removed !== true).map(([key]) => key).sort();
|
|
@@ -422,6 +1019,89 @@ export {};
|
|
|
422
1019
|
`;
|
|
423
1020
|
}
|
|
424
1021
|
|
|
1022
|
+
// src/db/schema-json.ts
|
|
1023
|
+
function defOf(column) {
|
|
1024
|
+
return "_def" in column ? column._def : column;
|
|
1025
|
+
}
|
|
1026
|
+
function columnToJSON(column) {
|
|
1027
|
+
const def = defOf(column);
|
|
1028
|
+
const out = {
|
|
1029
|
+
type: def.type,
|
|
1030
|
+
nullable: def.nullable,
|
|
1031
|
+
primaryKey: def.primaryKey
|
|
1032
|
+
};
|
|
1033
|
+
if (def.defaultValue !== void 0) out.defaultValue = def.defaultValue;
|
|
1034
|
+
if (def.defaultRandom === true) out.defaultRandom = true;
|
|
1035
|
+
if (def.defaultNow === true) out.defaultNow = true;
|
|
1036
|
+
if (def.renamedFrom !== void 0) out.renamedFrom = def.renamedFrom;
|
|
1037
|
+
if (def.references !== void 0) {
|
|
1038
|
+
out.references = { table: def.references.table, column: def.references.column };
|
|
1039
|
+
}
|
|
1040
|
+
if (def.onDeleteAction !== void 0) out.onDeleteAction = def.onDeleteAction;
|
|
1041
|
+
if (def.enumName !== void 0) out.enumName = def.enumName;
|
|
1042
|
+
if (def.enumValues !== void 0) out.enumValues = [...def.enumValues];
|
|
1043
|
+
if (def.unique === true) out.unique = true;
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
function policyToJSON(policy2) {
|
|
1047
|
+
return {
|
|
1048
|
+
name: policy2.name,
|
|
1049
|
+
command: policy2.command ?? "all",
|
|
1050
|
+
roles: policy2.roles ? [...policy2.roles] : [],
|
|
1051
|
+
// null rather than omitted: a policy with no USING clause is a different
|
|
1052
|
+
// thing from one whose clause the emitter forgot, and Go reads the
|
|
1053
|
+
// difference.
|
|
1054
|
+
using: policy2.using ?? null,
|
|
1055
|
+
withCheck: policy2.withCheck ?? null,
|
|
1056
|
+
permissive: policy2.permissive !== false
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function tableToJSON(table) {
|
|
1060
|
+
const columns = {};
|
|
1061
|
+
for (const [name, column] of Object.entries(table.columns)) {
|
|
1062
|
+
columns[name] = columnToJSON(column);
|
|
1063
|
+
}
|
|
1064
|
+
const out = {
|
|
1065
|
+
name: table.name,
|
|
1066
|
+
columns,
|
|
1067
|
+
// Read, not re-derived. `defineSchema` already resolves the fail-closed
|
|
1068
|
+
// default (RLS on unless the author wrote `rls: false`, and forced on by any
|
|
1069
|
+
// policy), and a second copy of a SECURITY default is exactly the thing that
|
|
1070
|
+
// drifts — the direction it drifted last time was "expose everything", and
|
|
1071
|
+
// the live proof was one user reading another's rows.
|
|
1072
|
+
rls: table.rls,
|
|
1073
|
+
policies: (table.policies ?? []).map(policyToJSON)
|
|
1074
|
+
};
|
|
1075
|
+
if (table.primaryKey !== void 0 && table.primaryKey.length > 0) {
|
|
1076
|
+
out.primaryKey = [...table.primaryKey];
|
|
1077
|
+
}
|
|
1078
|
+
if (table.unique !== void 0 && table.unique.length > 0) {
|
|
1079
|
+
out.uniqueConstraints = table.unique.map((u) => ({ name: u.name, columns: [...u.columns] }));
|
|
1080
|
+
}
|
|
1081
|
+
if (table.raw !== void 0 && table.raw.length > 0) {
|
|
1082
|
+
out.rawConstraints = table.raw.map((r) => ({
|
|
1083
|
+
name: r.name,
|
|
1084
|
+
up: r.up,
|
|
1085
|
+
down: r.down ?? null
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
if (table.checks !== void 0 && table.checks.length > 0) {
|
|
1089
|
+
out.checks = table.checks.map((c) => ({ name: c.name, expr: c.expr }));
|
|
1090
|
+
}
|
|
1091
|
+
if (table.indexes !== void 0 && table.indexes.length > 0) {
|
|
1092
|
+
out.indexes = table.indexes.map((i) => ({ name: i.name, columns: [...i.columns] }));
|
|
1093
|
+
}
|
|
1094
|
+
return out;
|
|
1095
|
+
}
|
|
1096
|
+
function toSchemaJSON(schema) {
|
|
1097
|
+
const tables = {};
|
|
1098
|
+
for (const table of Object.values(schema.tables)) {
|
|
1099
|
+
const json = tableToJSON(table);
|
|
1100
|
+
tables[json.name] = json;
|
|
1101
|
+
}
|
|
1102
|
+
return { tables, extensions: [...new Set(schema.extensions ?? [])] };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
425
1105
|
// src/db/env-gen.ts
|
|
426
1106
|
function baseTsType(def) {
|
|
427
1107
|
switch (def.type) {
|
|
@@ -552,6 +1232,7 @@ export {};
|
|
|
552
1232
|
// src/config/storage.ts
|
|
553
1233
|
var STORAGE_CONFIG_KIND = "storage";
|
|
554
1234
|
var VARIANT_FORMATS = ["webp", "avif", "jpeg", "png"];
|
|
1235
|
+
var VARIANT_FITS = ["cover", "contain", "inside"];
|
|
555
1236
|
function normalizeVariants(input) {
|
|
556
1237
|
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
557
1238
|
throw new Error("bucket variants must be an object of { <name>: { width?, height?, format? } }");
|
|
@@ -581,10 +1262,26 @@ function normalizeVariants(input) {
|
|
|
581
1262
|
`bucket variant "${name}" format must be one of ${VARIANT_FORMATS.join(", ")}, got "${String(spec.format)}"`
|
|
582
1263
|
);
|
|
583
1264
|
}
|
|
1265
|
+
if (spec.fit !== void 0 && !VARIANT_FITS.includes(spec.fit)) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`bucket variant "${name}" fit must be one of ${VARIANT_FITS.join(", ")}, got "${String(spec.fit)}"`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
if (spec.quality !== void 0) {
|
|
1271
|
+
const q = spec.quality;
|
|
1272
|
+
if (typeof q !== "number" || !Number.isInteger(q) || q < 1 || q > 100) {
|
|
1273
|
+
throw new Error(`bucket variant "${name}" quality must be an integer 1\u2013100, got ${String(q)}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (spec.fit === "cover" && (spec.width === void 0 || spec.height === void 0)) {
|
|
1277
|
+
throw new Error(`bucket variant "${name}" uses fit "cover", which needs BOTH width and height`);
|
|
1278
|
+
}
|
|
584
1279
|
out[name] = {
|
|
585
1280
|
...spec.width !== void 0 ? { width: spec.width } : {},
|
|
586
1281
|
...spec.height !== void 0 ? { height: spec.height } : {},
|
|
587
|
-
...spec.
|
|
1282
|
+
...spec.fit !== void 0 ? { fit: spec.fit } : {},
|
|
1283
|
+
...spec.format !== void 0 ? { format: spec.format } : {},
|
|
1284
|
+
...spec.quality !== void 0 ? { quality: spec.quality } : {}
|
|
588
1285
|
};
|
|
589
1286
|
}
|
|
590
1287
|
return out;
|
|
@@ -1115,138 +1812,55 @@ function defineFlags(input) {
|
|
|
1115
1812
|
return { __config: FLAGS_CONFIG_KIND, flags };
|
|
1116
1813
|
}
|
|
1117
1814
|
|
|
1118
|
-
// src/
|
|
1119
|
-
var
|
|
1120
|
-
var
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (!Object.prototype.hasOwnProperty.call(carrier, PARAM_BUFFER)) {
|
|
1135
|
-
carrier[PARAM_BUFFER] = {};
|
|
1136
|
-
}
|
|
1137
|
-
return carrier[PARAM_BUFFER];
|
|
1138
|
-
}
|
|
1139
|
-
function recordRoute(target, fnName, method, subpath, options) {
|
|
1140
|
-
const carrier = carrierOf2(target);
|
|
1141
|
-
const routes = ownRoutes(carrier);
|
|
1142
|
-
const buffer = ownParamBuffer(carrier);
|
|
1143
|
-
const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);
|
|
1144
|
-
const route = { method, subpath, fnName, options, params };
|
|
1145
|
-
const returnBuffer = carrier[RETURN_BUFFER];
|
|
1146
|
-
if (returnBuffer && returnBuffer[fnName] !== void 0) {
|
|
1147
|
-
route.returnSchema = returnBuffer[fnName];
|
|
1148
|
-
}
|
|
1149
|
-
const throwsBuffer = carrier[THROWS_BUFFER];
|
|
1150
|
-
if (throwsBuffer && throwsBuffer[fnName] !== void 0) {
|
|
1151
|
-
route.throws = throwsBuffer[fnName];
|
|
1152
|
-
}
|
|
1153
|
-
routes.push(route);
|
|
1154
|
-
}
|
|
1155
|
-
function recordParam(target, fnName, meta) {
|
|
1156
|
-
const carrier = carrierOf2(target);
|
|
1157
|
-
const buffer = ownParamBuffer(carrier);
|
|
1158
|
-
(buffer[fnName] ??= []).push(meta);
|
|
1159
|
-
const routes = carrier[ROUTES];
|
|
1160
|
-
if (routes) {
|
|
1161
|
-
const route = routes.find((r) => r.fnName === fnName);
|
|
1162
|
-
if (route) {
|
|
1163
|
-
route.params.push(meta);
|
|
1164
|
-
route.params.sort((a, b) => a.index - b.index);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
function recordThrows(target, fnName, throws) {
|
|
1169
|
-
const carrier = carrierOf2(target);
|
|
1170
|
-
const routes = carrier[ROUTES];
|
|
1171
|
-
const route = routes?.find((r) => r.fnName === fnName);
|
|
1172
|
-
if (route) {
|
|
1173
|
-
route.throws = throws;
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
if (!Object.prototype.hasOwnProperty.call(carrier, THROWS_BUFFER)) {
|
|
1177
|
-
carrier[THROWS_BUFFER] = {};
|
|
1178
|
-
}
|
|
1179
|
-
const throwsBuffer = carrier[THROWS_BUFFER];
|
|
1180
|
-
if (throwsBuffer) throwsBuffer[fnName] = throws;
|
|
1181
|
-
}
|
|
1182
|
-
function getRoutes(ctor) {
|
|
1183
|
-
const carrier = carrierOf2(ctor);
|
|
1184
|
-
const routes = carrier[ROUTES] ?? [];
|
|
1185
|
-
const returnBuffer = carrier[RETURN_BUFFER];
|
|
1186
|
-
if (returnBuffer) {
|
|
1187
|
-
for (const route of routes) {
|
|
1188
|
-
const buffered = returnBuffer[route.fnName];
|
|
1189
|
-
if (buffered && route.returnSchema === void 0) {
|
|
1190
|
-
route.returnSchema = buffered;
|
|
1191
|
-
}
|
|
1815
|
+
// src/config/secrets.ts
|
|
1816
|
+
var SECRETS_CONFIG_KIND = "secrets";
|
|
1817
|
+
var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
1818
|
+
function secret(name, opts = {}) {
|
|
1819
|
+
if (typeof name !== "string" || !SECRET_NAME_RE.test(name)) {
|
|
1820
|
+
throw new Error(
|
|
1821
|
+
`secret name ${JSON.stringify(name)} is invalid \u2014 must be an environment-variable name: an uppercase letter, then uppercase letters, digits, and underscores (e.g. "NEO4J_PASSWORD")`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
if (opts === null || typeof opts !== "object") {
|
|
1825
|
+
throw new Error(`secret("${name}") expects { required?, description? }`);
|
|
1826
|
+
}
|
|
1827
|
+
let required = true;
|
|
1828
|
+
if (opts.required !== void 0) {
|
|
1829
|
+
if (typeof opts.required !== "boolean") {
|
|
1830
|
+
throw new Error(`secret("${name}") required must be a boolean`);
|
|
1192
1831
|
}
|
|
1832
|
+
required = opts.required;
|
|
1193
1833
|
}
|
|
1194
|
-
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (buffered && route.throws === void 0) {
|
|
1199
|
-
route.throws = buffered;
|
|
1200
|
-
}
|
|
1834
|
+
let description = null;
|
|
1835
|
+
if (opts.description !== void 0) {
|
|
1836
|
+
if (typeof opts.description !== "string") {
|
|
1837
|
+
throw new Error(`secret("${name}") description must be a string`);
|
|
1201
1838
|
}
|
|
1839
|
+
const trimmed = opts.description.trim();
|
|
1840
|
+
description = trimmed.length > 0 ? trimmed : null;
|
|
1202
1841
|
}
|
|
1203
|
-
return
|
|
1204
|
-
...r,
|
|
1205
|
-
params: r.params.slice(),
|
|
1206
|
-
...r.throws !== void 0 ? { throws: r.throws.slice() } : {}
|
|
1207
|
-
}));
|
|
1842
|
+
return { name, required, description };
|
|
1208
1843
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
var RESERVED_FIRST_SEGMENT = "webhooks";
|
|
1213
|
-
function assertNotReserved(path, subject) {
|
|
1214
|
-
const [first] = path.split("/").filter(Boolean);
|
|
1215
|
-
if (first === RESERVED_FIRST_SEGMENT) {
|
|
1216
|
-
throw new Error(
|
|
1217
|
-
`${subject} resolves under the reserved /${RESERVED_FIRST_SEGMENT} path \u2014 inbound webhooks are served there and would shadow this route`
|
|
1218
|
-
);
|
|
1844
|
+
function defineSecrets(input) {
|
|
1845
|
+
if (input === null || typeof input !== "object" || !Array.isArray(input.secrets)) {
|
|
1846
|
+
throw new Error('defineSecrets expects { secrets: [secret("NAME"), ...] }');
|
|
1219
1847
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
assertNotReserved(
|
|
1226
|
-
`${basePath}${route.subpath}`,
|
|
1227
|
-
`@${route.method}("${route.subpath}") in @Controller("${basePath}")`
|
|
1848
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1849
|
+
for (const def of input.secrets) {
|
|
1850
|
+
if (def === null || typeof def !== "object" || typeof def.name !== "string" || typeof def.required !== "boolean") {
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`defineSecrets: every entry must come from secret() \u2014 got ${JSON.stringify(def)}`
|
|
1228
1853
|
);
|
|
1229
1854
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
configurable: true,
|
|
1240
|
-
writable: false
|
|
1241
|
-
});
|
|
1242
|
-
Object.defineProperty(carrier, "__palbase", {
|
|
1243
|
-
value: "controller",
|
|
1244
|
-
enumerable: false,
|
|
1245
|
-
configurable: true,
|
|
1246
|
-
writable: false
|
|
1247
|
-
});
|
|
1248
|
-
return ctor;
|
|
1249
|
-
};
|
|
1855
|
+
if (byName.has(def.name)) {
|
|
1856
|
+
throw new Error(
|
|
1857
|
+
`secret "${def.name}" is declared twice in config/secrets.ts \u2014 remove one; a name has exactly one requirement`
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
byName.set(def.name, def);
|
|
1861
|
+
}
|
|
1862
|
+
const secrets = [...byName.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1863
|
+
return { __config: SECRETS_CONFIG_KIND, secrets };
|
|
1250
1864
|
}
|
|
1251
1865
|
|
|
1252
1866
|
// src/decorators/methods.ts
|
|
@@ -1358,149 +1972,15 @@ function defineMiddleware(fn) {
|
|
|
1358
1972
|
return fn;
|
|
1359
1973
|
}
|
|
1360
1974
|
|
|
1361
|
-
// src/error-registry.ts
|
|
1362
|
-
import {
|
|
1363
|
-
OpenAPIRegistry,
|
|
1364
|
-
OpenApiGeneratorV31,
|
|
1365
|
-
extendZodWithOpenApi
|
|
1366
|
-
} from "@asteasolutions/zod-to-openapi";
|
|
1367
|
-
import { z } from "zod";
|
|
1368
|
-
extendZodWithOpenApi(z);
|
|
1369
|
-
var ERROR_REGISTRY = /* @__PURE__ */ Symbol.for("palbase.backend.errorRegistry");
|
|
1370
|
-
var BUILTIN_DATA_SCHEMAS = {
|
|
1371
|
-
bad_request: z.object({
|
|
1372
|
-
fields: z.array(z.object({ field: z.string(), message: z.string() }))
|
|
1373
|
-
}),
|
|
1374
|
-
too_many_requests: z.object({ retryAfter: z.number().int() }),
|
|
1375
|
-
// Purchases (@RequireEntitlement / @Spend). Their payloads are what make a
|
|
1376
|
-
// paywall renderable on the client: WHICH entitlement was missing, and for a
|
|
1377
|
-
// spend the real ceiling and reset time rather than a guessed retry delay.
|
|
1378
|
-
entitlement_required: z.object({ entitlement: z.string().nullable() }),
|
|
1379
|
-
quota_exceeded: z.object({
|
|
1380
|
-
key: z.string(),
|
|
1381
|
-
scope: z.string(),
|
|
1382
|
-
window: z.string(),
|
|
1383
|
-
used: z.number().int(),
|
|
1384
|
-
reserved: z.number().int(),
|
|
1385
|
-
max: z.number().int(),
|
|
1386
|
-
remaining: z.number().int(),
|
|
1387
|
-
resetAt: z.string()
|
|
1388
|
-
}),
|
|
1389
|
-
credit_insufficient: z.object({
|
|
1390
|
-
key: z.string(),
|
|
1391
|
-
balance: z.number().int(),
|
|
1392
|
-
reserved: z.number().int(),
|
|
1393
|
-
remaining: z.number().int()
|
|
1394
|
-
})
|
|
1395
|
-
};
|
|
1396
|
-
function getErrorRegistry() {
|
|
1397
|
-
const g = globalThis;
|
|
1398
|
-
if (!g[ERROR_REGISTRY]) {
|
|
1399
|
-
const m = /* @__PURE__ */ new Map();
|
|
1400
|
-
for (const [code, status, className] of [
|
|
1401
|
-
["bad_request", 400, "BadRequest"],
|
|
1402
|
-
["unauthorized", 401, "Unauthorized"],
|
|
1403
|
-
["forbidden", 403, "Forbidden"],
|
|
1404
|
-
["not_found", 404, "NotFound"],
|
|
1405
|
-
["conflict", 409, "Conflict"],
|
|
1406
|
-
["too_many_requests", 429, "TooManyRequests"],
|
|
1407
|
-
// Thrown by the purchases decorators, so they are pre-seeded like the
|
|
1408
|
-
// other built-ins: a project never declares them, but every gated or
|
|
1409
|
-
// metered route must surface them as TYPED errors on the client.
|
|
1410
|
-
["entitlement_required", 403, "EntitlementRequired"],
|
|
1411
|
-
["quota_exceeded", 429, "QuotaExceeded"],
|
|
1412
|
-
["credit_insufficient", 429, "CreditInsufficient"]
|
|
1413
|
-
]) {
|
|
1414
|
-
const dataSchema = BUILTIN_DATA_SCHEMAS[code];
|
|
1415
|
-
m.set(code, {
|
|
1416
|
-
code,
|
|
1417
|
-
status,
|
|
1418
|
-
className,
|
|
1419
|
-
builtin: true,
|
|
1420
|
-
...dataSchema ? { dataSchema, dataDigest: digestOf(code, dataSchema) } : {}
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
g[ERROR_REGISTRY] = m;
|
|
1424
|
-
}
|
|
1425
|
-
return g[ERROR_REGISTRY];
|
|
1426
|
-
}
|
|
1427
|
-
function defineError(code, status, dataSchema) {
|
|
1428
|
-
if (!Number.isInteger(status) || status < 400 || status > 599) {
|
|
1429
|
-
throw new Error(
|
|
1430
|
-
`defineError: status for "${code}" must be a 4xx/5xx integer, got ${status} \u2014 error responses must not clobber success responses in the project spec.`
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
const registry2 = getErrorRegistry();
|
|
1434
|
-
const existing = registry2.get(code);
|
|
1435
|
-
const className = defaultClassName(code);
|
|
1436
|
-
const dataDigest = dataSchema ? digestOf(code, dataSchema) : void 0;
|
|
1437
|
-
if (existing) {
|
|
1438
|
-
const sameShape = existing.status === status && !existing.builtin && existing.dataDigest === dataDigest;
|
|
1439
|
-
if (!sameShape) {
|
|
1440
|
-
throw new Error(
|
|
1441
|
-
`defineError: duplicate error code "${code}" with a different shape (existing: status ${existing.status}${existing.builtin ? ", built-in" : ""}${existing.status === status && !existing.builtin ? ", different data schema" : ""}). Error codes are project-unique.`
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
const makeWithData = (schema) => class extends HttpError {
|
|
1446
|
-
static code = code;
|
|
1447
|
-
static status = status;
|
|
1448
|
-
constructor(data, message) {
|
|
1449
|
-
super(status, code, message ?? humanize(code), schema.parse(data));
|
|
1450
|
-
this.name = className;
|
|
1451
|
-
}
|
|
1452
|
-
};
|
|
1453
|
-
const makeWithoutData = () => class extends HttpError {
|
|
1454
|
-
static code = code;
|
|
1455
|
-
static status = status;
|
|
1456
|
-
constructor(message) {
|
|
1457
|
-
super(status, code, message ?? humanize(code));
|
|
1458
|
-
this.name = className;
|
|
1459
|
-
}
|
|
1460
|
-
};
|
|
1461
|
-
const cls = dataSchema ? makeWithData(dataSchema) : makeWithoutData();
|
|
1462
|
-
Object.defineProperty(cls, "name", { value: className });
|
|
1463
|
-
registry2.set(code, {
|
|
1464
|
-
code,
|
|
1465
|
-
status,
|
|
1466
|
-
className,
|
|
1467
|
-
builtin: false,
|
|
1468
|
-
...dataSchema ? { dataSchema } : {},
|
|
1469
|
-
...dataDigest !== void 0 ? { dataDigest } : {}
|
|
1470
|
-
});
|
|
1471
|
-
return cls;
|
|
1472
|
-
}
|
|
1473
|
-
function digestOf(code, dataSchema) {
|
|
1474
|
-
const TMP_REF = "__PalbaseErrorDataDigest";
|
|
1475
|
-
const tmpRegistry = new OpenAPIRegistry();
|
|
1476
|
-
tmpRegistry.register(TMP_REF, z.object({ data: dataSchema }).openapi(TMP_REF));
|
|
1477
|
-
const generated = new OpenApiGeneratorV31(tmpRegistry.definitions).generateComponents();
|
|
1478
|
-
const out = generated.components?.schemas?.[TMP_REF];
|
|
1479
|
-
const digest = JSON.stringify(out ?? null);
|
|
1480
|
-
if (digest.includes('"$ref"')) {
|
|
1481
|
-
throw new Error(
|
|
1482
|
-
`defineError: dataSchema for "${code}" carries .openapi(refId) metadata \u2014 it would emit dangling $ref pointers in the project spec. Use a plain zod schema (z.object({...})) without .openapi(...).`
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
return digest;
|
|
1486
|
-
}
|
|
1487
|
-
function defaultClassName(code) {
|
|
1488
|
-
return code.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
1489
|
-
}
|
|
1490
|
-
function humanize(code) {
|
|
1491
|
-
const s = code.replace(/_/g, " ");
|
|
1492
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
1975
|
// src/decorators/webhook.ts
|
|
1496
1976
|
var WEBHOOK_META = /* @__PURE__ */ Symbol.for("palbase.backend.webhookMeta");
|
|
1497
1977
|
var WEBHOOK_EVENTS = /* @__PURE__ */ Symbol.for("palbase.backend.webhookEvents");
|
|
1498
|
-
function
|
|
1978
|
+
function carrierOf(ctor) {
|
|
1499
1979
|
return ctor;
|
|
1500
1980
|
}
|
|
1501
1981
|
function Webhook(options) {
|
|
1502
1982
|
return function(ctor) {
|
|
1503
|
-
const carrier =
|
|
1983
|
+
const carrier = carrierOf(ctor);
|
|
1504
1984
|
Object.defineProperty(carrier, WEBHOOK_META, {
|
|
1505
1985
|
value: options,
|
|
1506
1986
|
enumerable: false,
|
|
@@ -1518,7 +1998,7 @@ function Webhook(options) {
|
|
|
1518
1998
|
}
|
|
1519
1999
|
function On(event) {
|
|
1520
2000
|
return function(target, fnName) {
|
|
1521
|
-
const carrier =
|
|
2001
|
+
const carrier = carrierOf(target.constructor);
|
|
1522
2002
|
const existing = carrier[WEBHOOK_EVENTS];
|
|
1523
2003
|
const entries = existing ? [...existing] : [];
|
|
1524
2004
|
entries.push({ event, fnName: String(fnName) });
|
|
@@ -1531,7 +2011,7 @@ function On(event) {
|
|
|
1531
2011
|
};
|
|
1532
2012
|
}
|
|
1533
2013
|
function getWebhookConfig(ctor) {
|
|
1534
|
-
const carrier =
|
|
2014
|
+
const carrier = carrierOf(ctor);
|
|
1535
2015
|
const meta = carrier[WEBHOOK_META];
|
|
1536
2016
|
const entries = carrier[WEBHOOK_EVENTS] ?? [];
|
|
1537
2017
|
if (!meta) {
|
|
@@ -1789,7 +2269,7 @@ var documents = {
|
|
|
1789
2269
|
};
|
|
1790
2270
|
|
|
1791
2271
|
// src/index.ts
|
|
1792
|
-
import { z
|
|
2272
|
+
import { z } from "zod";
|
|
1793
2273
|
export {
|
|
1794
2274
|
BadRequest,
|
|
1795
2275
|
Body,
|
|
@@ -1822,6 +2302,7 @@ export {
|
|
|
1822
2302
|
PALBASE_EXTENSIONS,
|
|
1823
2303
|
PROVIDER_CATALOG,
|
|
1824
2304
|
PalError,
|
|
2305
|
+
PalbaseModuleError,
|
|
1825
2306
|
Param,
|
|
1826
2307
|
Patch,
|
|
1827
2308
|
PolicyBuilder,
|
|
@@ -1836,7 +2317,9 @@ export {
|
|
|
1836
2317
|
RequestId,
|
|
1837
2318
|
RequireEntitlement,
|
|
1838
2319
|
Resource,
|
|
2320
|
+
SECRETS_CONFIG_KIND,
|
|
1839
2321
|
STORAGE_CONFIG_KIND,
|
|
2322
|
+
Secrets,
|
|
1840
2323
|
Spend,
|
|
1841
2324
|
Storage,
|
|
1842
2325
|
TEST_USERS_CONFIG_KIND,
|
|
@@ -1852,6 +2335,7 @@ export {
|
|
|
1852
2335
|
__getRuntime,
|
|
1853
2336
|
__registerResource,
|
|
1854
2337
|
__requestALS,
|
|
2338
|
+
__resetRegisteredControllers,
|
|
1855
2339
|
__runResourceBoot,
|
|
1856
2340
|
__runWithRuntime,
|
|
1857
2341
|
__setRuntime,
|
|
@@ -1860,6 +2344,7 @@ export {
|
|
|
1860
2344
|
bigint,
|
|
1861
2345
|
boolean,
|
|
1862
2346
|
bucket,
|
|
2347
|
+
buildModuleClients,
|
|
1863
2348
|
buildProvider,
|
|
1864
2349
|
dec,
|
|
1865
2350
|
defineEgress,
|
|
@@ -1868,6 +2353,7 @@ export {
|
|
|
1868
2353
|
defineMiddleware,
|
|
1869
2354
|
defineNotifications,
|
|
1870
2355
|
defineSchema,
|
|
2356
|
+
defineSecrets,
|
|
1871
2357
|
defineStorage,
|
|
1872
2358
|
defineTestUsers,
|
|
1873
2359
|
documents,
|
|
@@ -1877,6 +2363,7 @@ export {
|
|
|
1877
2363
|
flag,
|
|
1878
2364
|
getErrorRegistry,
|
|
1879
2365
|
getJobConfig,
|
|
2366
|
+
getRegisteredControllers,
|
|
1880
2367
|
getRoutes,
|
|
1881
2368
|
getWebhookConfig,
|
|
1882
2369
|
inc,
|
|
@@ -1884,6 +2371,7 @@ export {
|
|
|
1884
2371
|
isPalbaseExtension,
|
|
1885
2372
|
jsonb,
|
|
1886
2373
|
makeEnvDts,
|
|
2374
|
+
makeHttpClient,
|
|
1887
2375
|
makePurchasesDts,
|
|
1888
2376
|
makeTypedDB,
|
|
1889
2377
|
now,
|
|
@@ -1893,13 +2381,15 @@ export {
|
|
|
1893
2381
|
raw,
|
|
1894
2382
|
recordThrows,
|
|
1895
2383
|
reservedSecretKey,
|
|
2384
|
+
secret,
|
|
1896
2385
|
spendFor,
|
|
1897
2386
|
storage,
|
|
1898
2387
|
testUser,
|
|
1899
2388
|
text,
|
|
1900
2389
|
timestamp,
|
|
2390
|
+
toSchemaJSON,
|
|
1901
2391
|
uuid,
|
|
1902
2392
|
validateUploadAgainstStorage,
|
|
1903
|
-
|
|
2393
|
+
z
|
|
1904
2394
|
};
|
|
1905
2395
|
//# sourceMappingURL=index.js.map
|