@maravilla-labs/platform 0.2.1 → 0.2.4
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/config.d.ts +236 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -0
- package/dist/events.d.ts +182 -0
- package/dist/events.js +45 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +470 -51
- package/dist/index.js +253 -2
- package/dist/index.js.map +1 -1
- package/dist/push.d.ts +67 -0
- package/dist/push.js +173 -0
- package/dist/push.js.map +1 -0
- package/dist/ren-D0DCQ0Fs.d.ts +48 -0
- package/package.json +13 -1
- package/src/config.ts +276 -0
- package/src/events.ts +283 -0
- package/src/index.ts +1 -0
- package/src/push.ts +280 -0
- package/src/remote-client.ts +101 -1
- package/src/types.ts +514 -1
- package/tsup.config.ts +1 -1
package/dist/push.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/push.ts
|
|
2
|
+
var DEFAULT_BASE_PATH = "/_rt/push";
|
|
3
|
+
var DEFAULT_SW_PATH = "/_rt/push/sw.js";
|
|
4
|
+
var VISITOR_STORAGE_KEY = "maravilla.push.visitorId";
|
|
5
|
+
var REGISTER_TIMEOUT_MS = 1e4;
|
|
6
|
+
function assertPushSupported() {
|
|
7
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
|
|
8
|
+
throw new Error("Web Push is not supported: serviceWorker is unavailable");
|
|
9
|
+
}
|
|
10
|
+
if (typeof window === "undefined" || !("PushManager" in window)) {
|
|
11
|
+
throw new Error("Web Push is not supported: PushManager is unavailable");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function base64UrlToArrayBuffer(input) {
|
|
15
|
+
const padding = "=".repeat((4 - input.length % 4) % 4);
|
|
16
|
+
const base64 = (input + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
17
|
+
const raw = atob(base64);
|
|
18
|
+
const buffer = new ArrayBuffer(raw.length);
|
|
19
|
+
const view = new Uint8Array(buffer);
|
|
20
|
+
for (let i = 0; i < raw.length; i++) {
|
|
21
|
+
view[i] = raw.charCodeAt(i);
|
|
22
|
+
}
|
|
23
|
+
return buffer;
|
|
24
|
+
}
|
|
25
|
+
function arrayBufferToBase64Url(buffer) {
|
|
26
|
+
if (!buffer) return void 0;
|
|
27
|
+
const bytes = new Uint8Array(buffer);
|
|
28
|
+
let binary = "";
|
|
29
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
30
|
+
binary += String.fromCharCode(bytes[i]);
|
|
31
|
+
}
|
|
32
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
33
|
+
}
|
|
34
|
+
function randomUuid() {
|
|
35
|
+
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
36
|
+
if (c && typeof c.randomUUID === "function") {
|
|
37
|
+
return c.randomUUID();
|
|
38
|
+
}
|
|
39
|
+
const bytes = new Uint8Array(16);
|
|
40
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
41
|
+
c.getRandomValues(bytes);
|
|
42
|
+
} else {
|
|
43
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
44
|
+
}
|
|
45
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
46
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
47
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
48
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
49
|
+
}
|
|
50
|
+
function resolveVisitorId(userId, visitorId) {
|
|
51
|
+
if (visitorId) return visitorId;
|
|
52
|
+
if (userId) return null;
|
|
53
|
+
try {
|
|
54
|
+
const stored = window.localStorage.getItem(VISITOR_STORAGE_KEY);
|
|
55
|
+
if (stored) return stored;
|
|
56
|
+
const fresh = randomUuid();
|
|
57
|
+
window.localStorage.setItem(VISITOR_STORAGE_KEY, fresh);
|
|
58
|
+
return fresh;
|
|
59
|
+
} catch {
|
|
60
|
+
return randomUuid();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function fetchVapidPublicKey(basePath) {
|
|
64
|
+
const res = await fetch(`${basePath}/vapid-public-key`, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
credentials: "same-origin",
|
|
67
|
+
headers: { Accept: "application/json" }
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(`Failed to fetch VAPID public key: ${res.status} ${res.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
if (!body || typeof body.publicKey !== "string" || body.publicKey.length === 0) {
|
|
74
|
+
throw new Error("VAPID public key response is missing `publicKey`");
|
|
75
|
+
}
|
|
76
|
+
return body.publicKey;
|
|
77
|
+
}
|
|
78
|
+
function extractKeys(sub) {
|
|
79
|
+
return {
|
|
80
|
+
p256dh: arrayBufferToBase64Url(sub.getKey("p256dh")),
|
|
81
|
+
auth: arrayBufferToBase64Url(sub.getKey("auth"))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function registerPush(opts = {}) {
|
|
85
|
+
assertPushSupported();
|
|
86
|
+
const basePath = opts.basePath ?? DEFAULT_BASE_PATH;
|
|
87
|
+
const swPath = opts.swPath ?? DEFAULT_SW_PATH;
|
|
88
|
+
const topics = opts.topics ?? [];
|
|
89
|
+
const userId = opts.userId ?? null;
|
|
90
|
+
const visitorId = resolveVisitorId(userId, opts.visitorId);
|
|
91
|
+
const timeout = new Promise(
|
|
92
|
+
(_, reject) => setTimeout(
|
|
93
|
+
() => reject(new Error(`registerPush timed out after ${REGISTER_TIMEOUT_MS}ms`)),
|
|
94
|
+
REGISTER_TIMEOUT_MS
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
const flow = (async () => {
|
|
98
|
+
const publicKey = await fetchVapidPublicKey(basePath);
|
|
99
|
+
const registration = await navigator.serviceWorker.register(swPath);
|
|
100
|
+
const existing = await registration.pushManager.getSubscription();
|
|
101
|
+
const subscription = existing ?? await registration.pushManager.subscribe({
|
|
102
|
+
userVisibleOnly: true,
|
|
103
|
+
applicationServerKey: base64UrlToArrayBuffer(publicKey)
|
|
104
|
+
});
|
|
105
|
+
const { p256dh, auth } = extractKeys(subscription);
|
|
106
|
+
const res = await fetch(`${basePath}/subscribe`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
credentials: "same-origin",
|
|
109
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
provider: "web-push",
|
|
112
|
+
endpoint: subscription.endpoint,
|
|
113
|
+
p256dh,
|
|
114
|
+
auth,
|
|
115
|
+
userId,
|
|
116
|
+
visitorId,
|
|
117
|
+
topics
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new Error(`Subscribe failed: ${res.status} ${res.statusText}`);
|
|
122
|
+
}
|
|
123
|
+
const saved = await res.json();
|
|
124
|
+
if (!saved || typeof saved.id !== "string" || saved.id.length === 0) {
|
|
125
|
+
throw new Error("Subscribe response is missing `id`");
|
|
126
|
+
}
|
|
127
|
+
return { subscription, subscriptionId: saved.id };
|
|
128
|
+
})();
|
|
129
|
+
return Promise.race([flow, timeout]);
|
|
130
|
+
}
|
|
131
|
+
async function unregisterPush(subscriptionId, opts = {}) {
|
|
132
|
+
if (!subscriptionId) {
|
|
133
|
+
throw new Error("subscriptionId is required");
|
|
134
|
+
}
|
|
135
|
+
const basePath = opts.basePath ?? DEFAULT_BASE_PATH;
|
|
136
|
+
const res = await fetch(`${basePath}/unsubscribe`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
credentials: "same-origin",
|
|
139
|
+
headers: { "Content-Type": "application/json" },
|
|
140
|
+
body: JSON.stringify({ subscriptionId })
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok && res.status !== 404) {
|
|
143
|
+
throw new Error(`Unsubscribe failed: ${res.status} ${res.statusText}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function offsetBefore(anchor, offset) {
|
|
147
|
+
const anchorDate = anchor instanceof Date ? anchor : new Date(anchor);
|
|
148
|
+
if (Number.isNaN(anchorDate.getTime())) {
|
|
149
|
+
throw new Error(`offsetBefore: invalid anchor "${String(anchor)}"`);
|
|
150
|
+
}
|
|
151
|
+
const match = /^(\d+)\s*(s|m|h|d|w)$/i.exec(offset.trim());
|
|
152
|
+
if (!match) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`offsetBefore: invalid offset "${offset}" \u2014 expected something like "30m", "1h", "2d", "1w"`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const amount = Number(match[1]);
|
|
158
|
+
const unit = match[2].toLowerCase();
|
|
159
|
+
const UNIT_MS = {
|
|
160
|
+
s: 1e3,
|
|
161
|
+
m: 6e4,
|
|
162
|
+
h: 36e5,
|
|
163
|
+
d: 864e5,
|
|
164
|
+
w: 6048e5
|
|
165
|
+
};
|
|
166
|
+
return new Date(anchorDate.getTime() - amount * UNIT_MS[unit]);
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
offsetBefore,
|
|
170
|
+
registerPush,
|
|
171
|
+
unregisterPush
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=push.js.map
|
package/dist/push.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/push.ts"],"sourcesContent":["/**\n * @fileoverview Web Push client SDK for Maravilla tenants.\n *\n * Talks to the tenant-origin `/_rt/push` endpoints served by delivery:\n * - GET /vapid-public-key — fetch the VAPID public key for subscribe\n * - POST /subscribe — create a subscription\n * - POST /unsubscribe — delete a subscription by id (or endpoint)\n *\n * The matching service worker is served from `/_rt/push/sw.js` on the\n * same tenant origin (browsers require same-origin SW registration).\n */\n\nconst DEFAULT_BASE_PATH = '/_rt/push';\nconst DEFAULT_SW_PATH = '/_rt/push/sw.js';\nconst VISITOR_STORAGE_KEY = 'maravilla.push.visitorId';\nconst REGISTER_TIMEOUT_MS = 10_000;\n\nexport interface RegisterPushOptions {\n /** Free-form topic strings to tag this subscription with. */\n topics?: string[];\n /** Authenticated user id, if any. Takes precedence over visitorId. */\n userId?: string | null;\n /** Anonymous visitor id. If omitted and userId is also omitted, one\n * is generated and persisted to localStorage under `maravilla.push.visitorId`. */\n visitorId?: string | null;\n /** Override the sw.js path (defaults to `/_platform/push/sw.js`). */\n swPath?: string;\n /** Override the API base path (defaults to `/_platform/push`). */\n basePath?: string;\n}\n\nexport interface RegisterPushResult {\n /** The browser PushSubscription (see Web Push spec). */\n subscription: PushSubscription;\n /** Server-issued subscription id — pass this back to unregisterPush. */\n subscriptionId: string;\n}\n\ninterface VapidPublicKeyResponse {\n publicKey: string;\n contactEmail?: string;\n}\n\ninterface ServerSubscription {\n id: string;\n [key: string]: unknown;\n}\n\nfunction assertPushSupported(): void {\n if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {\n throw new Error('Web Push is not supported: serviceWorker is unavailable');\n }\n if (typeof window === 'undefined' || !('PushManager' in window)) {\n throw new Error('Web Push is not supported: PushManager is unavailable');\n }\n}\n\nfunction base64UrlToArrayBuffer(input: string): ArrayBuffer {\n const padding = '='.repeat((4 - (input.length % 4)) % 4);\n const base64 = (input + padding).replace(/-/g, '+').replace(/_/g, '/');\n const raw = atob(base64);\n const buffer = new ArrayBuffer(raw.length);\n const view = new Uint8Array(buffer);\n for (let i = 0; i < raw.length; i++) {\n view[i] = raw.charCodeAt(i);\n }\n return buffer;\n}\n\nfunction arrayBufferToBase64Url(buffer: ArrayBuffer | null): string | undefined {\n if (!buffer) return undefined;\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction randomUuid(): string {\n const c = typeof crypto !== 'undefined' ? crypto : undefined;\n if (c && typeof c.randomUUID === 'function') {\n return c.randomUUID();\n }\n const bytes = new Uint8Array(16);\n if (c && typeof c.getRandomValues === 'function') {\n c.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\nfunction resolveVisitorId(\n userId: string | null | undefined,\n visitorId: string | null | undefined,\n): string | null {\n if (visitorId) return visitorId;\n if (userId) return null;\n try {\n const stored = window.localStorage.getItem(VISITOR_STORAGE_KEY);\n if (stored) return stored;\n const fresh = randomUuid();\n window.localStorage.setItem(VISITOR_STORAGE_KEY, fresh);\n return fresh;\n } catch {\n return randomUuid();\n }\n}\n\nasync function fetchVapidPublicKey(basePath: string): Promise<string> {\n const res = await fetch(`${basePath}/vapid-public-key`, {\n method: 'GET',\n credentials: 'same-origin',\n headers: { Accept: 'application/json' },\n });\n if (!res.ok) {\n throw new Error(`Failed to fetch VAPID public key: ${res.status} ${res.statusText}`);\n }\n const body = (await res.json()) as VapidPublicKeyResponse;\n if (!body || typeof body.publicKey !== 'string' || body.publicKey.length === 0) {\n throw new Error('VAPID public key response is missing `publicKey`');\n }\n return body.publicKey;\n}\n\nfunction extractKeys(sub: PushSubscription): { p256dh?: string; auth?: string } {\n return {\n p256dh: arrayBufferToBase64Url(sub.getKey('p256dh')),\n auth: arrayBufferToBase64Url(sub.getKey('auth')),\n };\n}\n\nexport async function registerPush(\n opts: RegisterPushOptions = {},\n): Promise<RegisterPushResult> {\n assertPushSupported();\n\n const basePath = opts.basePath ?? DEFAULT_BASE_PATH;\n const swPath = opts.swPath ?? DEFAULT_SW_PATH;\n const topics = opts.topics ?? [];\n const userId = opts.userId ?? null;\n const visitorId = resolveVisitorId(userId, opts.visitorId);\n\n // Whole-flow timeout. Without this, a stuck pushManager.subscribe() or a\n // misbehaving push service can leave the UI hanging forever.\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`registerPush timed out after ${REGISTER_TIMEOUT_MS}ms`)),\n REGISTER_TIMEOUT_MS,\n ),\n );\n\n const flow = (async (): Promise<RegisterPushResult> => {\n const publicKey = await fetchVapidPublicKey(basePath);\n // Register the SW. We deliberately do NOT `await navigator.serviceWorker.ready`\n // here — that promise only resolves when an active SW's scope covers the\n // current page, but our SW is scoped to `/_rt/push/` and the page is at\n // `/`. `pushManager.subscribe()` works against the registration returned\n // by `register()` without needing the SW to control the page.\n const registration = await navigator.serviceWorker.register(swPath);\n\n const existing = await registration.pushManager.getSubscription();\n const subscription =\n existing ??\n (await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: base64UrlToArrayBuffer(publicKey),\n }));\n\n const { p256dh, auth } = extractKeys(subscription);\n\n const res = await fetch(`${basePath}/subscribe`, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({\n provider: 'web-push',\n endpoint: subscription.endpoint,\n p256dh,\n auth,\n userId,\n visitorId,\n topics,\n }),\n });\n\n if (!res.ok) {\n throw new Error(`Subscribe failed: ${res.status} ${res.statusText}`);\n }\n\n const saved = (await res.json()) as ServerSubscription;\n if (!saved || typeof saved.id !== 'string' || saved.id.length === 0) {\n throw new Error('Subscribe response is missing `id`');\n }\n\n return { subscription, subscriptionId: saved.id };\n })();\n\n return Promise.race([flow, timeout]);\n}\n\nexport async function unregisterPush(\n subscriptionId: string,\n opts: { basePath?: string } = {},\n): Promise<void> {\n if (!subscriptionId) {\n throw new Error('subscriptionId is required');\n }\n const basePath = opts.basePath ?? DEFAULT_BASE_PATH;\n\n const res = await fetch(`${basePath}/unsubscribe`, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ subscriptionId }),\n });\n\n if (!res.ok && res.status !== 404) {\n throw new Error(`Unsubscribe failed: ${res.status} ${res.statusText}`);\n }\n}\n\n/**\n * Compute a `Date` that is `offset` before `anchor` — handy for\n * \"X minutes/hours/days before the event\" scheduling.\n *\n * `offset` is a short duration string:\n * - `\"30s\"` — 30 seconds\n * - `\"15m\"` — 15 minutes\n * - `\"1h\"` — 1 hour\n * - `\"2d\"` — 2 days\n * - `\"1w\"` — 1 week\n *\n * Pure function — safe to call on the client. Works with `platform.push.schedule`:\n *\n * @example\n * ```typescript\n * import { offsetBefore } from '@maravilla-labs/platform';\n *\n * await platform.push.schedule(\n * { topic: `invite:${invite.id}` },\n * { title: invite.title, body: 'Your event is in one hour' },\n * {\n * at: offsetBefore(invite.event_date, '1h'),\n * key: `invite:${invite.id}:reminder-1h`,\n * }\n * );\n * ```\n *\n * @throws if `anchor` is an invalid date string or `offset` isn't a\n * recognised duration.\n */\nexport function offsetBefore(anchor: Date | string, offset: string): Date {\n const anchorDate = anchor instanceof Date ? anchor : new Date(anchor);\n if (Number.isNaN(anchorDate.getTime())) {\n throw new Error(`offsetBefore: invalid anchor \"${String(anchor)}\"`);\n }\n\n const match = /^(\\d+)\\s*(s|m|h|d|w)$/i.exec(offset.trim());\n if (!match) {\n throw new Error(\n `offsetBefore: invalid offset \"${offset}\" — expected something like \"30m\", \"1h\", \"2d\", \"1w\"`,\n );\n }\n\n const amount = Number(match[1]);\n const unit = match[2].toLowerCase();\n const UNIT_MS: Record<string, number> = {\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n };\n return new Date(anchorDate.getTime() - amount * UNIT_MS[unit]);\n}\n"],"mappings":";AAYA,IAAM,oBAAoB;AAC1B,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB;AAiC5B,SAAS,sBAA4B;AACnC,MAAI,OAAO,cAAc,eAAe,EAAE,mBAAmB,YAAY;AACvE,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACA,MAAI,OAAO,WAAW,eAAe,EAAE,iBAAiB,SAAS;AAC/D,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACF;AAEA,SAAS,uBAAuB,OAA4B;AAC1D,QAAM,UAAU,IAAI,QAAQ,IAAK,MAAM,SAAS,KAAM,CAAC;AACvD,QAAM,UAAU,QAAQ,SAAS,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACrE,QAAM,MAAM,KAAK,MAAM;AACvB,QAAM,SAAS,IAAI,YAAY,IAAI,MAAM;AACzC,QAAM,OAAO,IAAI,WAAW,MAAM;AAClC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,SAAK,CAAC,IAAI,IAAI,WAAW,CAAC;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,SAAS,uBAAuB,QAAgD;AAC9E,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,IAAI,WAAW,MAAM;AACnC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAEA,SAAS,aAAqB;AAC5B,QAAM,IAAI,OAAO,WAAW,cAAc,SAAS;AACnD,MAAI,KAAK,OAAO,EAAE,eAAe,YAAY;AAC3C,WAAO,EAAE,WAAW;AAAA,EACtB;AACA,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,MAAI,KAAK,OAAO,EAAE,oBAAoB,YAAY;AAChD,MAAE,gBAAgB,KAAK;AAAA,EACzB,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,IAAI,IAAK,OAAM,CAAC,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AAAA,EACxE;AACA,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,CAAC,IAAK,MAAM,CAAC,IAAI,KAAQ;AAC/B,QAAM,MAAM,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC7E,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,EAAE,CAAC;AAC1G;AAEA,SAAS,iBACP,QACA,WACe;AACf,MAAI,UAAW,QAAO;AACtB,MAAI,OAAQ,QAAO;AACnB,MAAI;AACF,UAAM,SAAS,OAAO,aAAa,QAAQ,mBAAmB;AAC9D,QAAI,OAAQ,QAAO;AACnB,UAAM,QAAQ,WAAW;AACzB,WAAO,aAAa,QAAQ,qBAAqB,KAAK;AACtD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,WAAW;AAAA,EACpB;AACF;AAEA,eAAe,oBAAoB,UAAmC;AACpE,QAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,qBAAqB;AAAA,IACtD,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,EAAE,QAAQ,mBAAmB;AAAA,EACxC,CAAC;AACD,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,MAAM,qCAAqC,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EACrF;AACA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,MAAI,CAAC,QAAQ,OAAO,KAAK,cAAc,YAAY,KAAK,UAAU,WAAW,GAAG;AAC9E,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO,KAAK;AACd;AAEA,SAAS,YAAY,KAA2D;AAC9E,SAAO;AAAA,IACL,QAAQ,uBAAuB,IAAI,OAAO,QAAQ,CAAC;AAAA,IACnD,MAAM,uBAAuB,IAAI,OAAO,MAAM,CAAC;AAAA,EACjD;AACF;AAEA,eAAsB,aACpB,OAA4B,CAAC,GACA;AAC7B,sBAAoB;AAEpB,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,SAAS,KAAK,UAAU,CAAC;AAC/B,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,YAAY,iBAAiB,QAAQ,KAAK,SAAS;AAIzD,QAAM,UAAU,IAAI;AAAA,IAAe,CAAC,GAAG,WACrC;AAAA,MACE,MAAM,OAAO,IAAI,MAAM,gCAAgC,mBAAmB,IAAI,CAAC;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,YAAyC;AACrD,UAAM,YAAY,MAAM,oBAAoB,QAAQ;AAMpD,UAAM,eAAe,MAAM,UAAU,cAAc,SAAS,MAAM;AAElE,UAAM,WAAW,MAAM,aAAa,YAAY,gBAAgB;AAChE,UAAM,eACJ,YACC,MAAM,aAAa,YAAY,UAAU;AAAA,MACxC,iBAAiB;AAAA,MACjB,sBAAsB,uBAAuB,SAAS;AAAA,IACxD,CAAC;AAEH,UAAM,EAAE,QAAQ,KAAK,IAAI,YAAY,YAAY;AAEjD,UAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,cAAc;AAAA,MAC/C,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,oBAAoB,QAAQ,mBAAmB;AAAA,MAC1E,MAAM,KAAK,UAAU;AAAA,QACnB,UAAU;AAAA,QACV,UAAU,aAAa;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,MAAM,qBAAqB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,IACrE;AAEA,UAAM,QAAS,MAAM,IAAI,KAAK;AAC9B,QAAI,CAAC,SAAS,OAAO,MAAM,OAAO,YAAY,MAAM,GAAG,WAAW,GAAG;AACnE,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAEA,WAAO,EAAE,cAAc,gBAAgB,MAAM,GAAG;AAAA,EAClD,GAAG;AAEH,SAAO,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;AACrC;AAEA,eAAsB,eACpB,gBACA,OAA8B,CAAC,GAChB;AACf,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AACA,QAAM,WAAW,KAAK,YAAY;AAElC,QAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,gBAAgB;AAAA,IACjD,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,EAAE,eAAe,CAAC;AAAA,EACzC,CAAC;AAED,MAAI,CAAC,IAAI,MAAM,IAAI,WAAW,KAAK;AACjC,UAAM,IAAI,MAAM,uBAAuB,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,EACvE;AACF;AAgCO,SAAS,aAAa,QAAuB,QAAsB;AACxE,QAAM,aAAa,kBAAkB,OAAO,SAAS,IAAI,KAAK,MAAM;AACpE,MAAI,OAAO,MAAM,WAAW,QAAQ,CAAC,GAAG;AACtC,UAAM,IAAI,MAAM,iCAAiC,OAAO,MAAM,CAAC,GAAG;AAAA,EACpE;AAEA,QAAM,QAAQ,yBAAyB,KAAK,OAAO,KAAK,CAAC;AACzD,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iCAAiC,MAAM;AAAA,IACzC;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,MAAM,CAAC,CAAC;AAC9B,QAAM,OAAO,MAAM,CAAC,EAAE,YAAY;AAClC,QAAM,UAAkC;AAAA,IACtC,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AACA,SAAO,IAAI,KAAK,WAAW,QAAQ,IAAI,SAAS,QAAQ,IAAI,CAAC;AAC/D;","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface RenEvent {
|
|
2
|
+
t: string;
|
|
3
|
+
r: string;
|
|
4
|
+
k?: string;
|
|
5
|
+
v?: string;
|
|
6
|
+
ts?: number;
|
|
7
|
+
src?: string;
|
|
8
|
+
ns?: string;
|
|
9
|
+
ch?: string;
|
|
10
|
+
data?: any;
|
|
11
|
+
uid?: string;
|
|
12
|
+
[extra: string]: any;
|
|
13
|
+
}
|
|
14
|
+
interface RenClientOptions {
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
subscriptions?: string[];
|
|
17
|
+
clientId?: string;
|
|
18
|
+
autoReconnect?: boolean;
|
|
19
|
+
maxBackoffMs?: number;
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
}
|
|
22
|
+
type Listener = (event: RenEvent) => void;
|
|
23
|
+
declare class RenClient {
|
|
24
|
+
private endpoint;
|
|
25
|
+
private subs;
|
|
26
|
+
private clientId;
|
|
27
|
+
private listeners;
|
|
28
|
+
private es;
|
|
29
|
+
private closed;
|
|
30
|
+
private attempt;
|
|
31
|
+
private autoReconnect;
|
|
32
|
+
private maxBackoff;
|
|
33
|
+
private debug;
|
|
34
|
+
constructor(opts?: RenClientOptions);
|
|
35
|
+
private detectEndpoint;
|
|
36
|
+
private log;
|
|
37
|
+
private buildUrl;
|
|
38
|
+
private connect;
|
|
39
|
+
on(listener: Listener): () => void;
|
|
40
|
+
getClientId(): string;
|
|
41
|
+
close(): void;
|
|
42
|
+
}
|
|
43
|
+
declare function getOrCreateClientId(storage?: Storage): string;
|
|
44
|
+
declare function renFetch(input: string | URL | Request, init?: RequestInit, clientId?: string): Promise<Response>;
|
|
45
|
+
declare function storageUpload(path: string, file: Blob | File, clientId?: string): Promise<any>;
|
|
46
|
+
declare function storageDelete(path: string, clientId?: string): Promise<any>;
|
|
47
|
+
|
|
48
|
+
export { type RenEvent as R, RenClient as a, type RenClientOptions as b, storageUpload as c, getOrCreateClientId as g, renFetch as r, storageDelete as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maravilla-labs/platform",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Universal platform client for Maravilla runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./config": {
|
|
15
|
+
"types": "./dist/config.d.ts",
|
|
16
|
+
"import": "./dist/config.js"
|
|
17
|
+
},
|
|
18
|
+
"./push": {
|
|
19
|
+
"types": "./dist/push.d.ts",
|
|
20
|
+
"import": "./dist/push.js"
|
|
21
|
+
},
|
|
22
|
+
"./events": {
|
|
23
|
+
"types": "./dist/events.d.ts",
|
|
24
|
+
"import": "./dist/events.js"
|
|
13
25
|
}
|
|
14
26
|
},
|
|
15
27
|
"scripts": {
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Typed schema for `maravilla.config.{ts,yaml,json}` files.
|
|
3
|
+
*
|
|
4
|
+
* Declares your project's auth settings (resources, groups, relations,
|
|
5
|
+
* registration fields, OAuth providers, security policy, branding) alongside
|
|
6
|
+
* your code. The Maravilla adapter reads this at build time and reconciles
|
|
7
|
+
* the settings into delivery on deploy.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { defineConfig } from '@maravilla-labs/platform/config';
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* auth: {
|
|
14
|
+
* resources: [
|
|
15
|
+
* { name: 'todos', title: 'Todos', actions: ['read', 'write'],
|
|
16
|
+
* policy: 'auth.user_id == node.owner' },
|
|
17
|
+
* ],
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Omitted sections leave the DB alone — partial adoption is explicitly
|
|
23
|
+
* supported. List-based sections (`resources`, `groups`, `relations`,
|
|
24
|
+
* `oauth`) are upserted and never auto-delete DB-only entries. Singleton
|
|
25
|
+
* sections (`registration`, `security`, `branding`) are replaced wholesale
|
|
26
|
+
* when declared.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* String value that may either be a literal secret or a reference to an
|
|
31
|
+
* environment variable on the **tenant** (resolved server-side at
|
|
32
|
+
* reconcile time, never shipped plaintext in the manifest).
|
|
33
|
+
*
|
|
34
|
+
* Accepted forms:
|
|
35
|
+
* - `"literal-value"` — inline (not recommended for real secrets)
|
|
36
|
+
* - `"${env.VAR_NAME}"` — string-template form
|
|
37
|
+
* - `{ env: "VAR_NAME" }` — object form
|
|
38
|
+
*/
|
|
39
|
+
export type SecretRef = string | { env: string };
|
|
40
|
+
|
|
41
|
+
// ── Resources + policies ──
|
|
42
|
+
|
|
43
|
+
export interface ResourceDefinition {
|
|
44
|
+
/** URL-safe slug. Used as the resource key in code (e.g. the KV namespace). */
|
|
45
|
+
name: string;
|
|
46
|
+
/** Human-readable title for the admin UI. */
|
|
47
|
+
title: string;
|
|
48
|
+
/** Optional longer description. */
|
|
49
|
+
description?: string;
|
|
50
|
+
/** Actions this resource supports, e.g. `['read', 'write', 'delete']`. */
|
|
51
|
+
actions: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Optional raisin-rel policy expression. Evaluated on every KV/DB/
|
|
54
|
+
* realtime/media op that targets this resource. Leave empty to skip
|
|
55
|
+
* Layer 2 for this resource — tenant + owner isolation still applies.
|
|
56
|
+
*/
|
|
57
|
+
policy?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Groups ──
|
|
61
|
+
|
|
62
|
+
export interface GroupPermissionDefinition {
|
|
63
|
+
/** Must match a `ResourceDefinition.name`. */
|
|
64
|
+
resource_name: string;
|
|
65
|
+
/** Actions this group is granted on the resource. */
|
|
66
|
+
actions: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GroupDefinition {
|
|
70
|
+
/** Unique group name per tenant. */
|
|
71
|
+
name: string;
|
|
72
|
+
/** Optional description for the admin UI. */
|
|
73
|
+
description?: string;
|
|
74
|
+
/** Resource permissions granted to the group. Replaces the group's current permissions when declared. */
|
|
75
|
+
permissions?: GroupPermissionDefinition[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Relations ──
|
|
79
|
+
|
|
80
|
+
export interface RelationTypeDefinition {
|
|
81
|
+
/** Uppercase identifier used in policies (`... VIA 'STEWARDS'`). */
|
|
82
|
+
relation_name: string;
|
|
83
|
+
/** Human-readable title. */
|
|
84
|
+
title: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
/** Grouping for the admin UI (e.g. `"family"`, `"work"`). */
|
|
87
|
+
category?: string;
|
|
88
|
+
icon?: string;
|
|
89
|
+
color?: string;
|
|
90
|
+
/** Name of the inverse relation type, if one exists. */
|
|
91
|
+
inverse_relation_name?: string;
|
|
92
|
+
/** When true, membership in this relation implies stewardship rights. */
|
|
93
|
+
implies_stewardship?: boolean;
|
|
94
|
+
/** When true, the relation can only target users flagged as minors. */
|
|
95
|
+
requires_minor?: boolean;
|
|
96
|
+
/** When true, the relation is symmetric (A→B implies B→A). */
|
|
97
|
+
bidirectional?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Registration fields ──
|
|
101
|
+
|
|
102
|
+
export interface RegistrationFieldDefinition {
|
|
103
|
+
/** Field key used as the form field name + in profile data. */
|
|
104
|
+
key: string;
|
|
105
|
+
/** Display label. */
|
|
106
|
+
label: string;
|
|
107
|
+
/** One of: text, email, phone, date, number, select, boolean, url, textarea. */
|
|
108
|
+
field_type: string;
|
|
109
|
+
required: boolean;
|
|
110
|
+
show_on_register: boolean;
|
|
111
|
+
/** Optional validation metadata — passed through to the UI. */
|
|
112
|
+
validation?: Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface RegistrationConfig {
|
|
116
|
+
/** Ordered list of custom registration fields. Declaring this replaces the full list. */
|
|
117
|
+
fields: RegistrationFieldDefinition[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── OAuth providers ──
|
|
121
|
+
|
|
122
|
+
export interface OAuthProviderDefinition {
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
client_id: string;
|
|
125
|
+
/** Prefer `{ env: "VAR_NAME" }` or `"${env.VAR_NAME}"`. */
|
|
126
|
+
client_secret: SecretRef;
|
|
127
|
+
scopes: string[];
|
|
128
|
+
/** Only for `custom_oidc`. */
|
|
129
|
+
discovery_url?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface OAuthProvidersConfig {
|
|
133
|
+
google?: OAuthProviderDefinition;
|
|
134
|
+
github?: OAuthProviderDefinition;
|
|
135
|
+
okta?: OAuthProviderDefinition;
|
|
136
|
+
custom_oidc?: OAuthProviderDefinition;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Security ──
|
|
140
|
+
|
|
141
|
+
export interface PasswordPolicyDefinition {
|
|
142
|
+
min_length: number;
|
|
143
|
+
require_uppercase: boolean;
|
|
144
|
+
require_number: boolean;
|
|
145
|
+
require_special: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface SessionConfigDefinition {
|
|
149
|
+
access_token_ttl_secs: number;
|
|
150
|
+
refresh_token_ttl_secs: number;
|
|
151
|
+
max_sessions_per_user: number;
|
|
152
|
+
require_email_verification: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SecurityConfig {
|
|
156
|
+
password_policy?: PasswordPolicyDefinition;
|
|
157
|
+
session?: SessionConfigDefinition;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Branding ──
|
|
161
|
+
|
|
162
|
+
export interface BrandingConfig {
|
|
163
|
+
app_name?: string;
|
|
164
|
+
logo_url?: string;
|
|
165
|
+
primary_color?: string;
|
|
166
|
+
secondary_color?: string;
|
|
167
|
+
welcome_message?: string;
|
|
168
|
+
welcome_subtitle?: string;
|
|
169
|
+
/** `"centered"`, `"split-left"`, `"split-right"`, or `"fullscreen"`. */
|
|
170
|
+
layout?: string;
|
|
171
|
+
background_image_url?: string;
|
|
172
|
+
/** 0–100 percentage. */
|
|
173
|
+
background_focal_point?: { x: number; y: number };
|
|
174
|
+
background_gradient?: string;
|
|
175
|
+
/** `"light"`, `"dark"`, or `"auto"`. */
|
|
176
|
+
color_mode?: string;
|
|
177
|
+
font_family?: string;
|
|
178
|
+
terms_url?: string;
|
|
179
|
+
privacy_url?: string;
|
|
180
|
+
/** Raw CSS merged into the hosted auth pages. */
|
|
181
|
+
custom_css?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Top-level shape ──
|
|
185
|
+
|
|
186
|
+
export interface AuthConfigBlock {
|
|
187
|
+
resources?: ResourceDefinition[];
|
|
188
|
+
groups?: GroupDefinition[];
|
|
189
|
+
relations?: RelationTypeDefinition[];
|
|
190
|
+
registration?: RegistrationConfig;
|
|
191
|
+
oauth?: OAuthProvidersConfig;
|
|
192
|
+
security?: SecurityConfig;
|
|
193
|
+
branding?: BrandingConfig;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface MaravillaConfig {
|
|
197
|
+
/** All project-level auth settings. Every field is optional — partial adoption is supported. */
|
|
198
|
+
auth?: AuthConfigBlock;
|
|
199
|
+
/** Declarative database indexes (regular + vector). Reconciled upsert-only on deploy. */
|
|
200
|
+
database?: DatabaseConfigBlock;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Database block ──
|
|
204
|
+
//
|
|
205
|
+
// Regular indexes speed up document reads on frequently-queried fields.
|
|
206
|
+
// Vector indexes back hybrid semantic search via sqlite-vec. Both are
|
|
207
|
+
// upsert-only — declaring an index in config creates it if missing,
|
|
208
|
+
// updates metadata when safe, and never auto-deletes DB-only indexes.
|
|
209
|
+
|
|
210
|
+
/** MongoDB-style key direction: `1` ascending, `-1` descending. */
|
|
211
|
+
export type IndexDirectionConfig = 1 | -1;
|
|
212
|
+
|
|
213
|
+
export interface DocumentIndexDeclaration {
|
|
214
|
+
/** Collection the index lives on. */
|
|
215
|
+
collection: string;
|
|
216
|
+
/** Optional name; falls back to an auto-derived name. */
|
|
217
|
+
name?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Compound-index key shape. Array of `[field, direction]` tuples
|
|
220
|
+
* preserves ordering, which matters for compound indexes.
|
|
221
|
+
*/
|
|
222
|
+
keys: Array<[string, IndexDirectionConfig]> | Record<string, IndexDirectionConfig>;
|
|
223
|
+
unique?: boolean;
|
|
224
|
+
sparse?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Partial-index predicate — restricted to inline-literal operators
|
|
227
|
+
* (`$eq`, `$ne`, `$gt`/`$gte`/`$lt`/`$lte`, `$in`/`$nin`, `$exists`,
|
|
228
|
+
* `$and`, `$or`). No `$regex` / `$where` / `$text`.
|
|
229
|
+
*/
|
|
230
|
+
partial?: Record<string, unknown>;
|
|
231
|
+
/** TTL in seconds. Requires a single-field index on a unix-seconds field. */
|
|
232
|
+
expireAfterSeconds?: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Distance metric used by a vector index. */
|
|
236
|
+
export type VectorMetricConfig = 'cosine' | 'l2' | 'hamming';
|
|
237
|
+
|
|
238
|
+
/** Storage precision for a vector index. */
|
|
239
|
+
export type VectorStorageConfig = 'float32' | 'int8' | 'bit';
|
|
240
|
+
|
|
241
|
+
export interface VectorIndexDeclaration {
|
|
242
|
+
collection: string;
|
|
243
|
+
field: string;
|
|
244
|
+
dimensions: number;
|
|
245
|
+
metric?: VectorMetricConfig;
|
|
246
|
+
storage?: VectorStorageConfig;
|
|
247
|
+
matryoshka?: boolean;
|
|
248
|
+
multiVector?: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface DatabaseConfigBlock {
|
|
252
|
+
/** MongoDB-style secondary indexes. */
|
|
253
|
+
indexes?: DocumentIndexDeclaration[];
|
|
254
|
+
/** sqlite-vec-backed vector indexes. */
|
|
255
|
+
vectorIndexes?: VectorIndexDeclaration[];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Identity function that returns the config unchanged — exists purely so the
|
|
260
|
+
* TypeScript compiler can infer `MaravillaConfig` and give you IntelliSense
|
|
261
|
+
* on every field.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* import { defineConfig } from '@maravilla-labs/platform/config';
|
|
266
|
+
*
|
|
267
|
+
* export default defineConfig({
|
|
268
|
+
* auth: {
|
|
269
|
+
* resources: [{ name: 'todos', title: 'Todos', actions: ['read', 'write'] }],
|
|
270
|
+
* },
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
export function defineConfig(config: MaravillaConfig): MaravillaConfig {
|
|
275
|
+
return config;
|
|
276
|
+
}
|