@objectstack/runtime 9.8.0 → 9.9.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/index.cjs +239 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +239 -15
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
package/dist/index.d.cts
CHANGED
|
@@ -1567,6 +1567,26 @@ declare class HttpDispatcher {
|
|
|
1567
1567
|
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
1568
1568
|
*/
|
|
1569
1569
|
handleAI(subPath: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1570
|
+
/**
|
|
1571
|
+
* Share-link capability tokens — "anyone with the link" publication of a
|
|
1572
|
+
* single record (ADR-0047). Mirrors the per-env service-dispatch pattern
|
|
1573
|
+
* used by {@link handleI18n} / {@link handleAI}: the `shareLinks` service
|
|
1574
|
+
* is resolved from the request's environment kernel, so links live in (and
|
|
1575
|
+
* resolve against) the same per-environment database that owns the record.
|
|
1576
|
+
* This branch owns URL parsing and the auth/public split.
|
|
1577
|
+
*
|
|
1578
|
+
* POST /share-links → create a link (authenticated)
|
|
1579
|
+
* GET /share-links?object&recordId → list the caller's links (authenticated)
|
|
1580
|
+
* DELETE /share-links/:idOrToken → revoke (authenticated)
|
|
1581
|
+
* GET /share-links/:token/resolve → resolve token → record (PUBLIC)
|
|
1582
|
+
* GET /share-links/:token/messages → ai_conversations messages (PUBLIC)
|
|
1583
|
+
*
|
|
1584
|
+
* The resolve / messages routes are intentionally public — the token IS
|
|
1585
|
+
* the authorisation. The underlying record is fetched with a SYSTEM
|
|
1586
|
+
* context (per-env RLS is bypassed because the token gates access), and
|
|
1587
|
+
* `redactFields` are stripped before the record leaves the server.
|
|
1588
|
+
*/
|
|
1589
|
+
handleShareLinks(subPath: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1570
1590
|
/**
|
|
1571
1591
|
* Main Dispatcher Entry Point
|
|
1572
1592
|
* Routes the request to the appropriate handler based on path and precedence
|
|
@@ -1876,8 +1896,9 @@ declare class QuickJSScriptRunner implements ScriptRunner {
|
|
|
1876
1896
|
* the body declared it; missing methods throw at call-time inside the VM
|
|
1877
1897
|
* with a clear diagnostic.
|
|
1878
1898
|
*
|
|
1879
|
-
* Host API methods are installed
|
|
1880
|
-
* so they may return Promises (real ObjectQL
|
|
1899
|
+
* Host API methods are installed as deferred-promise functions (see
|
|
1900
|
+
* {@link installApiMethod}) so they may return Promises (real ObjectQL
|
|
1901
|
+
* `find/count/insert/...` are async) without asyncify's single-unwind limit.
|
|
1881
1902
|
*/
|
|
1882
1903
|
private installCtx;
|
|
1883
1904
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1567,6 +1567,26 @@ declare class HttpDispatcher {
|
|
|
1567
1567
|
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
1568
1568
|
*/
|
|
1569
1569
|
handleAI(subPath: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1570
|
+
/**
|
|
1571
|
+
* Share-link capability tokens — "anyone with the link" publication of a
|
|
1572
|
+
* single record (ADR-0047). Mirrors the per-env service-dispatch pattern
|
|
1573
|
+
* used by {@link handleI18n} / {@link handleAI}: the `shareLinks` service
|
|
1574
|
+
* is resolved from the request's environment kernel, so links live in (and
|
|
1575
|
+
* resolve against) the same per-environment database that owns the record.
|
|
1576
|
+
* This branch owns URL parsing and the auth/public split.
|
|
1577
|
+
*
|
|
1578
|
+
* POST /share-links → create a link (authenticated)
|
|
1579
|
+
* GET /share-links?object&recordId → list the caller's links (authenticated)
|
|
1580
|
+
* DELETE /share-links/:idOrToken → revoke (authenticated)
|
|
1581
|
+
* GET /share-links/:token/resolve → resolve token → record (PUBLIC)
|
|
1582
|
+
* GET /share-links/:token/messages → ai_conversations messages (PUBLIC)
|
|
1583
|
+
*
|
|
1584
|
+
* The resolve / messages routes are intentionally public — the token IS
|
|
1585
|
+
* the authorisation. The underlying record is fetched with a SYSTEM
|
|
1586
|
+
* context (per-env RLS is bypassed because the token gates access), and
|
|
1587
|
+
* `redactFields` are stripped before the record leaves the server.
|
|
1588
|
+
*/
|
|
1589
|
+
handleShareLinks(subPath: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1570
1590
|
/**
|
|
1571
1591
|
* Main Dispatcher Entry Point
|
|
1572
1592
|
* Routes the request to the appropriate handler based on path and precedence
|
|
@@ -1876,8 +1896,9 @@ declare class QuickJSScriptRunner implements ScriptRunner {
|
|
|
1876
1896
|
* the body declared it; missing methods throw at call-time inside the VM
|
|
1877
1897
|
* with a clear diagnostic.
|
|
1878
1898
|
*
|
|
1879
|
-
* Host API methods are installed
|
|
1880
|
-
* so they may return Promises (real ObjectQL
|
|
1899
|
+
* Host API methods are installed as deferred-promise functions (see
|
|
1900
|
+
* {@link installApiMethod}) so they may return Promises (real ObjectQL
|
|
1901
|
+
* `find/count/insert/...` are async) without asyncify's single-unwind limit.
|
|
1881
1902
|
*/
|
|
1882
1903
|
private installCtx;
|
|
1883
1904
|
}
|
package/dist/index.js
CHANGED
|
@@ -206,7 +206,7 @@ import {
|
|
|
206
206
|
newAsyncContext
|
|
207
207
|
} from "quickjs-emscripten";
|
|
208
208
|
function installApiMethod(vm, parent, method, objectName, ctx, caps, required, origin) {
|
|
209
|
-
const fn = vm.
|
|
209
|
+
const fn = vm.newFunction(method, (...argHandles) => {
|
|
210
210
|
if (!caps.has(required)) {
|
|
211
211
|
throw new SandboxError(
|
|
212
212
|
`capability '${required}' not granted to ${origin.kind} '${origin.name}' (called ctx.api.object('${objectName}').${method})`
|
|
@@ -217,13 +217,27 @@ function installApiMethod(vm, parent, method, objectName, ctx, caps, required, o
|
|
|
217
217
|
throw new SandboxError(`ctx.api unavailable in ${origin.kind} '${origin.name}'`);
|
|
218
218
|
}
|
|
219
219
|
const args = argHandles.map((h) => vm.dump(h));
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
220
|
+
const deferred = vm.newPromise();
|
|
221
|
+
void (async () => {
|
|
222
|
+
try {
|
|
223
|
+
const proxy = apiAny.object(objectName);
|
|
224
|
+
const m = proxy[method];
|
|
225
|
+
if (typeof m !== "function") {
|
|
226
|
+
throw new SandboxError(`ctx.api.object('${objectName}').${method} not implemented`);
|
|
227
|
+
}
|
|
228
|
+
const ret = await Promise.resolve(m.apply(proxy, args));
|
|
229
|
+
if (!vm.alive) return;
|
|
230
|
+
const h = jsonToHandle(vm, ret);
|
|
231
|
+
deferred.resolve(h);
|
|
232
|
+
h.dispose();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (!vm.alive) return;
|
|
235
|
+
const errH = err instanceof Error ? vm.newError({ name: err.name || "Error", message: err.message }) : vm.newError({ name: "Error", message: String(err) });
|
|
236
|
+
deferred.reject(errH);
|
|
237
|
+
errH.dispose();
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
240
|
+
return deferred.handle;
|
|
227
241
|
});
|
|
228
242
|
vm.setProp(parent, method, fn);
|
|
229
243
|
fn.dispose();
|
|
@@ -385,7 +399,7 @@ var init_quickjs_runner = __esm({
|
|
|
385
399
|
}
|
|
386
400
|
evalRes.value.dispose();
|
|
387
401
|
let pumps = 0;
|
|
388
|
-
|
|
402
|
+
for (; ; ) {
|
|
389
403
|
await new Promise((resolve) => setImmediate(resolve));
|
|
390
404
|
const pending = runtime.executePendingJobs();
|
|
391
405
|
if (pending.error) {
|
|
@@ -409,14 +423,11 @@ var init_quickjs_runner = __esm({
|
|
|
409
423
|
}
|
|
410
424
|
if (Date.now() > deadline) {
|
|
411
425
|
throw new SandboxError(
|
|
412
|
-
`${args.origin.kind} '${args.origin.name}' exceeded timeout of ${args.timeoutMs}ms`
|
|
426
|
+
`${args.origin.kind} '${args.origin.name}' exceeded timeout of ${args.timeoutMs}ms (after ${pumps} pump iterations)`
|
|
413
427
|
);
|
|
414
428
|
}
|
|
415
429
|
pumps++;
|
|
416
430
|
}
|
|
417
|
-
throw new SandboxError(
|
|
418
|
-
`${args.origin.kind} '${args.origin.name}' did not resolve after ${pumps} pump iterations`
|
|
419
|
-
);
|
|
420
431
|
} finally {
|
|
421
432
|
vm.dispose();
|
|
422
433
|
}
|
|
@@ -426,8 +437,9 @@ var init_quickjs_runner = __esm({
|
|
|
426
437
|
* the body declared it; missing methods throw at call-time inside the VM
|
|
427
438
|
* with a clear diagnostic.
|
|
428
439
|
*
|
|
429
|
-
* Host API methods are installed
|
|
430
|
-
* so they may return Promises (real ObjectQL
|
|
440
|
+
* Host API methods are installed as deferred-promise functions (see
|
|
441
|
+
* {@link installApiMethod}) so they may return Promises (real ObjectQL
|
|
442
|
+
* `find/count/insert/...` are async) without asyncify's single-unwind limit.
|
|
431
443
|
*/
|
|
432
444
|
installCtx(vm, ctx, caps, origin) {
|
|
433
445
|
setGlobalJson(vm, "__input", ctx.input);
|
|
@@ -1939,6 +1951,43 @@ async function tryFind(ql, object, where, limit = 100) {
|
|
|
1939
1951
|
return [];
|
|
1940
1952
|
}
|
|
1941
1953
|
}
|
|
1954
|
+
function isValidTimeZone(tz) {
|
|
1955
|
+
try {
|
|
1956
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
1957
|
+
return true;
|
|
1958
|
+
} catch {
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function coerceTimeZone(value) {
|
|
1963
|
+
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
1964
|
+
return s && isValidTimeZone(s) ? s : void 0;
|
|
1965
|
+
}
|
|
1966
|
+
function coerceLocale(value) {
|
|
1967
|
+
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
1968
|
+
return s || void 0;
|
|
1969
|
+
}
|
|
1970
|
+
async function resolveLocalization(opts, ql, sctx) {
|
|
1971
|
+
try {
|
|
1972
|
+
const settings = await opts.getService("settings");
|
|
1973
|
+
if (settings && typeof settings.get === "function") {
|
|
1974
|
+
const [tzRes, localeRes] = await Promise.all([
|
|
1975
|
+
settings.get("localization", "timezone", sctx).catch(() => void 0),
|
|
1976
|
+
settings.get("localization", "locale", sctx).catch(() => void 0)
|
|
1977
|
+
]);
|
|
1978
|
+
const tz = coerceTimeZone(tzRes?.value);
|
|
1979
|
+
const locale = coerceLocale(localeRes?.value);
|
|
1980
|
+
if (tz || locale) return { timezone: tz ?? "UTC", locale: locale ?? "en-US" };
|
|
1981
|
+
}
|
|
1982
|
+
} catch {
|
|
1983
|
+
}
|
|
1984
|
+
const tzRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "timezone", scope: "tenant" }, 1);
|
|
1985
|
+
const localeRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "locale", scope: "tenant" }, 1);
|
|
1986
|
+
return {
|
|
1987
|
+
timezone: coerceTimeZone(tzRows[0]?.value) ?? "UTC",
|
|
1988
|
+
locale: coerceLocale(localeRows[0]?.value) ?? "en-US"
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1942
1991
|
async function resolveExecutionContext(opts) {
|
|
1943
1992
|
const headers = opts.request?.headers;
|
|
1944
1993
|
const ctx = {
|
|
@@ -2069,6 +2118,9 @@ async function resolveExecutionContext(opts) {
|
|
|
2069
2118
|
ctx.tabPermissions = mergedTabs;
|
|
2070
2119
|
}
|
|
2071
2120
|
}
|
|
2121
|
+
const localization = await resolveLocalization(opts, ql, { tenantId, userId });
|
|
2122
|
+
ctx.timezone = localization.timezone;
|
|
2123
|
+
ctx.locale = localization.locale;
|
|
2072
2124
|
return ctx;
|
|
2073
2125
|
}
|
|
2074
2126
|
function isPermissionDeniedError(e) {
|
|
@@ -2568,6 +2620,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2568
2620
|
if (!this.enforceMembership) return null;
|
|
2569
2621
|
const skipPaths = ["/auth", "/cloud", "/health", "/discovery"];
|
|
2570
2622
|
if (skipPaths.some((p) => path.startsWith(p))) return null;
|
|
2623
|
+
if (/(^|\/)share-links\/[^/]+\/(resolve|messages)$/.test(path)) return null;
|
|
2571
2624
|
const environmentId = context.environmentId;
|
|
2572
2625
|
if (!environmentId) return null;
|
|
2573
2626
|
if (environmentId === _HttpDispatcher.SYSTEM_ENVIRONMENT_ID) return null;
|
|
@@ -4164,6 +4217,174 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4164
4217
|
response: this.routeNotFound(subPath)
|
|
4165
4218
|
};
|
|
4166
4219
|
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Share-link capability tokens — "anyone with the link" publication of a
|
|
4222
|
+
* single record (ADR-0047). Mirrors the per-env service-dispatch pattern
|
|
4223
|
+
* used by {@link handleI18n} / {@link handleAI}: the `shareLinks` service
|
|
4224
|
+
* is resolved from the request's environment kernel, so links live in (and
|
|
4225
|
+
* resolve against) the same per-environment database that owns the record.
|
|
4226
|
+
* This branch owns URL parsing and the auth/public split.
|
|
4227
|
+
*
|
|
4228
|
+
* POST /share-links → create a link (authenticated)
|
|
4229
|
+
* GET /share-links?object&recordId → list the caller's links (authenticated)
|
|
4230
|
+
* DELETE /share-links/:idOrToken → revoke (authenticated)
|
|
4231
|
+
* GET /share-links/:token/resolve → resolve token → record (PUBLIC)
|
|
4232
|
+
* GET /share-links/:token/messages → ai_conversations messages (PUBLIC)
|
|
4233
|
+
*
|
|
4234
|
+
* The resolve / messages routes are intentionally public — the token IS
|
|
4235
|
+
* the authorisation. The underlying record is fetched with a SYSTEM
|
|
4236
|
+
* context (per-env RLS is bypassed because the token gates access), and
|
|
4237
|
+
* `redactFields` are stripped before the record leaves the server.
|
|
4238
|
+
*/
|
|
4239
|
+
async handleShareLinks(subPath, method, body, query, context) {
|
|
4240
|
+
const svc = await this.resolveService("shareLinks", context.environmentId);
|
|
4241
|
+
if (!svc) {
|
|
4242
|
+
return { handled: true, response: this.error("Sharing is not configured for this environment", 501) };
|
|
4243
|
+
}
|
|
4244
|
+
const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
|
|
4245
|
+
const m = method.toUpperCase();
|
|
4246
|
+
const parts = subPath.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
4247
|
+
const ec = context.executionContext;
|
|
4248
|
+
const callerCtx = { userId: ec?.userId, tenantId: ec?.tenantId };
|
|
4249
|
+
const headerOf = (name) => {
|
|
4250
|
+
const h = context.request?.headers;
|
|
4251
|
+
if (!h) return void 0;
|
|
4252
|
+
const v = typeof h.get === "function" ? h.get(name) : h[name] ?? h[name.toLowerCase()];
|
|
4253
|
+
return Array.isArray(v) ? v[0] : v ?? void 0;
|
|
4254
|
+
};
|
|
4255
|
+
const sendErr = (status, code, msg) => ({
|
|
4256
|
+
handled: true,
|
|
4257
|
+
response: this.error(msg, status, { code })
|
|
4258
|
+
});
|
|
4259
|
+
const getEngine = async () => {
|
|
4260
|
+
try {
|
|
4261
|
+
const k = this.kernel;
|
|
4262
|
+
const e = typeof k?.getServiceAsync === "function" ? await k.getServiceAsync("objectql") : k?.getService?.("objectql");
|
|
4263
|
+
if (e) return e;
|
|
4264
|
+
} catch {
|
|
4265
|
+
}
|
|
4266
|
+
return this.resolveService("objectql", context.environmentId);
|
|
4267
|
+
};
|
|
4268
|
+
const asArray = (rows) => Array.isArray(rows) ? rows : Array.isArray(rows?.value) ? rows.value : [];
|
|
4269
|
+
const applyRedaction = (record, redactFields) => {
|
|
4270
|
+
if (!record || typeof record !== "object" || redactFields.length === 0) return record;
|
|
4271
|
+
const out = {};
|
|
4272
|
+
for (const [k, v] of Object.entries(record)) {
|
|
4273
|
+
if (redactFields.includes(k)) continue;
|
|
4274
|
+
out[k] = v;
|
|
4275
|
+
}
|
|
4276
|
+
return out;
|
|
4277
|
+
};
|
|
4278
|
+
try {
|
|
4279
|
+
if (parts.length === 2 && parts[1] === "resolve" && m === "GET") {
|
|
4280
|
+
const token = decodeURIComponent(parts[0]);
|
|
4281
|
+
const signedInUserId = ec?.userId;
|
|
4282
|
+
const recipientEmail = typeof query?.email === "string" ? query.email : void 0;
|
|
4283
|
+
const providedPassword = typeof query?.password === "string" ? query.password : headerOf("x-share-password");
|
|
4284
|
+
const resolved = await svc.resolveToken(token, { signedInUserId, recipientEmail, providedPassword });
|
|
4285
|
+
if (!resolved) {
|
|
4286
|
+
const engine2 = await getEngine();
|
|
4287
|
+
const probe = engine2 ? asArray(await engine2.find("sys_share_link", { where: { token }, limit: 1, context: SYSTEM_CTX })) : [];
|
|
4288
|
+
const row = probe[0] ?? null;
|
|
4289
|
+
const live = row && !row.revoked_at && (!row.expires_at || Date.parse(row.expires_at) > Date.now());
|
|
4290
|
+
if (live && row.password_hash) {
|
|
4291
|
+
return sendErr(
|
|
4292
|
+
401,
|
|
4293
|
+
providedPassword ? "WRONG_PASSWORD" : "NEEDS_PASSWORD",
|
|
4294
|
+
providedPassword ? "Incorrect password" : "This link requires a password"
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
if (live && row.audience === "signed_in" && !signedInUserId) {
|
|
4298
|
+
return sendErr(401, "SIGN_IN_REQUIRED", "Please sign in to view this link");
|
|
4299
|
+
}
|
|
4300
|
+
if (row && (row.revoked_at || row.expires_at && Date.parse(row.expires_at) <= Date.now())) {
|
|
4301
|
+
return sendErr(410, "EXPIRED_OR_REVOKED", "Share link has expired or been revoked");
|
|
4302
|
+
}
|
|
4303
|
+
return sendErr(404, "INVALID_OR_EXPIRED", "Share link is invalid, expired, or revoked");
|
|
4304
|
+
}
|
|
4305
|
+
const engine = await getEngine();
|
|
4306
|
+
const rows = engine ? asArray(await engine.find(resolved.link.object_name, { where: { id: resolved.link.record_id }, limit: 1, context: SYSTEM_CTX })) : [];
|
|
4307
|
+
const record = rows[0] ?? null;
|
|
4308
|
+
if (!record) return sendErr(410, "RECORD_GONE", "The shared record no longer exists");
|
|
4309
|
+
return {
|
|
4310
|
+
handled: true,
|
|
4311
|
+
response: this.success({
|
|
4312
|
+
record: applyRedaction(record, resolved.redactFields),
|
|
4313
|
+
link: {
|
|
4314
|
+
id: resolved.link.id,
|
|
4315
|
+
token: resolved.link.token,
|
|
4316
|
+
object_name: resolved.link.object_name,
|
|
4317
|
+
record_id: resolved.link.record_id,
|
|
4318
|
+
permission: resolved.link.permission,
|
|
4319
|
+
audience: resolved.link.audience,
|
|
4320
|
+
expires_at: resolved.link.expires_at,
|
|
4321
|
+
label: resolved.link.label,
|
|
4322
|
+
created_at: resolved.link.created_at
|
|
4323
|
+
},
|
|
4324
|
+
redactFields: resolved.redactFields
|
|
4325
|
+
})
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
if (parts.length === 2 && parts[1] === "messages" && m === "GET") {
|
|
4329
|
+
const token = decodeURIComponent(parts[0]);
|
|
4330
|
+
const providedPassword = typeof query?.password === "string" ? query.password : headerOf("x-share-password");
|
|
4331
|
+
const resolved = await svc.resolveToken(token, { signedInUserId: ec?.userId, providedPassword });
|
|
4332
|
+
if (!resolved) return sendErr(404, "NOT_FOUND", "Share link not found");
|
|
4333
|
+
if (resolved.link.object_name !== "ai_conversations") {
|
|
4334
|
+
return sendErr(400, "UNSUPPORTED", "This share link does not expose messages");
|
|
4335
|
+
}
|
|
4336
|
+
const engine = await getEngine();
|
|
4337
|
+
const rows = engine ? asArray(await engine.find("ai_messages", {
|
|
4338
|
+
where: { conversation_id: resolved.link.record_id },
|
|
4339
|
+
sort: [{ field: "created_at", order: "asc" }],
|
|
4340
|
+
limit: 500,
|
|
4341
|
+
context: SYSTEM_CTX
|
|
4342
|
+
})) : [];
|
|
4343
|
+
return { handled: true, response: this.success(rows) };
|
|
4344
|
+
}
|
|
4345
|
+
if (!callerCtx.userId) return sendErr(401, "UNAUTHENTICATED", "Sign in to manage share links");
|
|
4346
|
+
if (parts.length === 0 && m === "POST") {
|
|
4347
|
+
const b = body ?? {};
|
|
4348
|
+
if (!b.object || !b.recordId) return sendErr(400, "VALIDATION_FAILED", "object and recordId are required");
|
|
4349
|
+
const link = await svc.createLink(
|
|
4350
|
+
{
|
|
4351
|
+
object: b.object,
|
|
4352
|
+
recordId: b.recordId,
|
|
4353
|
+
permission: b.permission,
|
|
4354
|
+
audience: b.audience,
|
|
4355
|
+
expiresAt: b.expiresAt ?? null,
|
|
4356
|
+
emailAllowlist: b.emailAllowlist,
|
|
4357
|
+
password: b.password,
|
|
4358
|
+
redactFields: b.redactFields,
|
|
4359
|
+
label: b.label
|
|
4360
|
+
},
|
|
4361
|
+
callerCtx
|
|
4362
|
+
);
|
|
4363
|
+
return { handled: true, response: { status: 201, body: { success: true, data: link, link } } };
|
|
4364
|
+
}
|
|
4365
|
+
if (parts.length === 0 && m === "GET") {
|
|
4366
|
+
const links = await svc.listLinks(
|
|
4367
|
+
{
|
|
4368
|
+
object: typeof query?.object === "string" ? query.object : void 0,
|
|
4369
|
+
recordId: typeof query?.recordId === "string" ? query.recordId : void 0,
|
|
4370
|
+
// Constrain to links the caller created so a guessed
|
|
4371
|
+
// recordId can never enumerate another user's tokens.
|
|
4372
|
+
createdBy: callerCtx.userId,
|
|
4373
|
+
includeRevoked: query?.includeRevoked === "true" || query?.includeRevoked === "1"
|
|
4374
|
+
},
|
|
4375
|
+
callerCtx
|
|
4376
|
+
);
|
|
4377
|
+
return { handled: true, response: { status: 200, body: { success: true, data: links, links } } };
|
|
4378
|
+
}
|
|
4379
|
+
if (parts.length === 1 && m === "DELETE") {
|
|
4380
|
+
await svc.revokeLink(decodeURIComponent(parts[0]), callerCtx);
|
|
4381
|
+
return { handled: true, response: this.success({ ok: true }) };
|
|
4382
|
+
}
|
|
4383
|
+
return { handled: true, response: this.routeNotFound(`/share-links${subPath}`) };
|
|
4384
|
+
} catch (err) {
|
|
4385
|
+
return sendErr(err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Share link request failed");
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4167
4388
|
/**
|
|
4168
4389
|
* Main Dispatcher Entry Point
|
|
4169
4390
|
* Routes the request to the appropriate handler based on path and precedence
|
|
@@ -4259,6 +4480,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4259
4480
|
if (cleanPath.startsWith("/ai")) {
|
|
4260
4481
|
return this.handleAI(cleanPath, method, body, query, context);
|
|
4261
4482
|
}
|
|
4483
|
+
if (cleanPath === "/share-links" || cleanPath.startsWith("/share-links/")) {
|
|
4484
|
+
return this.handleShareLinks(cleanPath.substring("/share-links".length), method, body, query, context);
|
|
4485
|
+
}
|
|
4262
4486
|
if (cleanPath === "/openapi.json" && method === "GET") {
|
|
4263
4487
|
try {
|
|
4264
4488
|
const metaSvc = await this.resolveService("metadata", context.environmentId);
|