mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
c89d2bf3a1
Lets a plugin colour-code days in the Plan sidebar. trip-segments can already announce "Kanazawa begins" in the day plan, but it can't make day 12 look like it belongs to Kanazawa. dayScheduleProvider can't cover this: it renders a fixed row, and it's capped at 60 items per provider, so one item per day would tint the first 60 days of a six-month trip and silently stop. This hook is bounded by day count instead. GET /api/day-tints/:tripId returns per-day tones for three regions of a day card - the number badge (also the mobile day chip), the header row, and the expanded activity list. `tone` is a shorthand filling every region a contribution doesn't name; unnamed regions render as they do today. Tones only, never hex, so the host can pick the alpha per theme and per region. One contribution per day, resolved whole: first entry wins within a provider, first granted provider wins across plugins.
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
/**
|
|
* The host <-> plugin JSON-RPC wire protocol (#plugins, M1).
|
|
*
|
|
* PURE TYPES ONLY — this file must never import anything with runtime side
|
|
* effects. It is loaded by BOTH the privileged host (parent process) and the
|
|
* isolated plugin child, and the child must not transitively pull in db, config
|
|
* or the websocket server. Keep it dependency-free.
|
|
*/
|
|
|
|
export type PluginErrCode =
|
|
| 'PERMISSION_DENIED' // a real method the plugin was not granted
|
|
| 'UNKNOWN_METHOD' // not a method the host exposes at all
|
|
| 'BAD_PARAMS' // params failed validation
|
|
| 'RESOURCE_FORBIDDEN' // granted, but the acting user can't touch this resource
|
|
| 'TIMEOUT'
|
|
| 'PLUGIN_ERROR'
|
|
| 'HOST_ERROR';
|
|
|
|
export interface RpcRequest {
|
|
k: 'req';
|
|
id: string;
|
|
method: string;
|
|
params: unknown;
|
|
}
|
|
export interface RpcResponse {
|
|
k: 'res';
|
|
id: string;
|
|
ok: true;
|
|
result: unknown;
|
|
}
|
|
export interface RpcError {
|
|
k: 'res';
|
|
id: string;
|
|
ok: false;
|
|
error: { code: PluginErrCode; message: string };
|
|
}
|
|
export interface RpcEvent {
|
|
k: 'evt';
|
|
topic: string;
|
|
data: unknown;
|
|
}
|
|
export type Envelope = RpcRequest | RpcResponse | RpcError | RpcEvent;
|
|
|
|
/**
|
|
* Every method the host CAN expose. The capability router registers only the
|
|
* subset a plugin was granted; anything here but ungranted resolves to
|
|
* PERMISSION_DENIED, anything not here at all resolves to UNKNOWN_METHOD.
|
|
*/
|
|
export const KNOWN_METHODS = [
|
|
'db.exec',
|
|
'db.query',
|
|
'db.migrate',
|
|
'db.tx',
|
|
'trips.getById',
|
|
'trips.getPlaces',
|
|
'trips.getReservations',
|
|
'trips.getDays',
|
|
'trips.getAccommodations',
|
|
'trips.listMine',
|
|
'reservations.listMine',
|
|
'reservations.create',
|
|
'reservations.update',
|
|
'reservations.delete',
|
|
'accommodations.create',
|
|
'accommodations.update',
|
|
'accommodations.delete',
|
|
'packing.list',
|
|
'packing.create',
|
|
'packing.update',
|
|
'packing.delete',
|
|
'packing.listBags',
|
|
'packing.createBag',
|
|
'packing.updateBag',
|
|
'packing.deleteBag',
|
|
'packing.setBagMembers',
|
|
'files.list',
|
|
'files.getContent',
|
|
'files.create',
|
|
'files.createLink',
|
|
'files.update',
|
|
'files.softDelete',
|
|
'collab.listNotes',
|
|
'collab.listPolls',
|
|
'collab.listMessages',
|
|
'collab.createNote',
|
|
'collab.createPoll',
|
|
'collab.votePoll',
|
|
'collab.createMessage',
|
|
'trips.addMember',
|
|
'trips.removeMember',
|
|
'trips.create',
|
|
'journal.listMine',
|
|
'journal.getEntries',
|
|
'atlas.visited',
|
|
'atlas.bucketList',
|
|
'rates.get',
|
|
'vacay.mine',
|
|
'daynotes.list',
|
|
'daynotes.create',
|
|
'daynotes.update',
|
|
'daynotes.delete',
|
|
'collections.listMine',
|
|
'collections.get',
|
|
'collections.create',
|
|
'collections.update',
|
|
'collections.savePlace',
|
|
'collections.copyToTrip',
|
|
'collections.deletePlace',
|
|
'atlas.markCountry',
|
|
'atlas.unmarkCountry',
|
|
'atlas.markRegion',
|
|
'atlas.unmarkRegion',
|
|
'atlas.createBucketItem',
|
|
'atlas.deleteBucketItem',
|
|
'vacay.toggleEntry',
|
|
'vacay.toggleCompanyHoliday',
|
|
'journal.createEntry',
|
|
'journal.updateEntry',
|
|
'journal.deleteEntry',
|
|
'journal.createJourney',
|
|
'journal.deleteJourney',
|
|
'weather.get',
|
|
'categories.list',
|
|
'tags.list',
|
|
'tags.create',
|
|
'tags.update',
|
|
'tags.delete',
|
|
'trips.members',
|
|
'todos.list',
|
|
'todos.create',
|
|
'todos.update',
|
|
'todos.delete',
|
|
'costs.getByTrip',
|
|
'costs.listMine',
|
|
'costs.create',
|
|
'costs.update',
|
|
'costs.delete',
|
|
'places.create',
|
|
'places.update',
|
|
'places.delete',
|
|
'days.create',
|
|
'days.update',
|
|
'days.delete',
|
|
'itinerary.assign',
|
|
'itinerary.unassign',
|
|
'trips.update',
|
|
'meta.get',
|
|
'meta.set',
|
|
'meta.list',
|
|
'meta.delete',
|
|
'users.getById',
|
|
'ws.broadcastToTrip',
|
|
'ws.broadcastToUser',
|
|
'notify.send',
|
|
'ai.complete',
|
|
'ai.extract',
|
|
'oauth.getToken',
|
|
'scheduler.set',
|
|
'scheduler.cancel',
|
|
] as const;
|
|
export type KnownMethod = (typeof KNOWN_METHODS)[number];
|
|
|
|
/** Which permission unlocks which method(s). The single source for the router. */
|
|
export const METHOD_PERMISSION: Record<KnownMethod, string> = {
|
|
'db.exec': 'db:own',
|
|
'db.query': 'db:own',
|
|
'db.migrate': 'db:own',
|
|
'db.tx': 'db:own',
|
|
'trips.getById': 'db:read:trips',
|
|
'trips.getPlaces': 'db:read:trips',
|
|
'trips.getReservations': 'db:read:trips',
|
|
'trips.getDays': 'db:read:trips',
|
|
'trips.getAccommodations': 'db:read:trips',
|
|
'trips.listMine': 'db:read:trips',
|
|
'reservations.listMine': 'db:read:trips',
|
|
'reservations.create': 'db:write:reservations',
|
|
'reservations.update': 'db:write:reservations',
|
|
'reservations.delete': 'db:write:reservations',
|
|
'accommodations.create': 'db:write:accommodations',
|
|
'accommodations.update': 'db:write:accommodations',
|
|
'accommodations.delete': 'db:write:accommodations',
|
|
'packing.list': 'db:read:packing',
|
|
'packing.create': 'db:write:packing',
|
|
'packing.update': 'db:write:packing',
|
|
'packing.delete': 'db:write:packing',
|
|
'packing.listBags': 'db:write:packing',
|
|
'packing.createBag': 'db:write:packing',
|
|
'packing.updateBag': 'db:write:packing',
|
|
'packing.deleteBag': 'db:write:packing',
|
|
'packing.setBagMembers': 'db:write:packing',
|
|
'files.list': 'db:read:files',
|
|
'files.getContent': 'db:read:files:content',
|
|
'files.create': 'db:write:files',
|
|
'files.createLink': 'db:write:files',
|
|
'files.update': 'db:write:files',
|
|
'files.softDelete': 'db:write:files',
|
|
'collab.listNotes': 'db:read:collab',
|
|
'collab.listPolls': 'db:read:collab',
|
|
'collab.listMessages': 'db:read:collab',
|
|
'collab.createNote': 'db:write:collab',
|
|
'collab.createPoll': 'db:write:collab',
|
|
'collab.votePoll': 'db:write:collab',
|
|
'collab.createMessage': 'db:write:collab',
|
|
'trips.addMember': 'db:write:members',
|
|
'trips.removeMember': 'db:write:members',
|
|
'trips.create': 'db:create:trips',
|
|
'journal.listMine': 'db:read:journal',
|
|
'journal.getEntries': 'db:read:journal',
|
|
'atlas.visited': 'db:read:atlas',
|
|
'atlas.bucketList': 'db:read:atlas',
|
|
'rates.get': 'rates:read',
|
|
'vacay.mine': 'db:read:vacay',
|
|
'daynotes.list': 'db:read:daynotes',
|
|
'daynotes.create': 'db:write:daynotes',
|
|
'daynotes.update': 'db:write:daynotes',
|
|
'daynotes.delete': 'db:write:daynotes',
|
|
'collections.listMine': 'db:read:collections',
|
|
'collections.get': 'db:read:collections',
|
|
'collections.create': 'db:write:collections',
|
|
'collections.update': 'db:write:collections',
|
|
'collections.savePlace': 'db:write:collections',
|
|
'collections.copyToTrip': 'db:write:collections',
|
|
'collections.deletePlace': 'db:write:collections',
|
|
'atlas.markCountry': 'db:write:atlas',
|
|
'atlas.unmarkCountry': 'db:write:atlas',
|
|
'atlas.markRegion': 'db:write:atlas',
|
|
'atlas.unmarkRegion': 'db:write:atlas',
|
|
'atlas.createBucketItem': 'db:write:atlas',
|
|
'atlas.deleteBucketItem': 'db:write:atlas',
|
|
'vacay.toggleEntry': 'db:write:vacay',
|
|
'vacay.toggleCompanyHoliday': 'db:write:vacay',
|
|
'journal.createEntry': 'db:write:journal',
|
|
'journal.updateEntry': 'db:write:journal',
|
|
'journal.deleteEntry': 'db:write:journal',
|
|
'journal.createJourney': 'db:write:journal',
|
|
'journal.deleteJourney': 'db:write:journal',
|
|
'weather.get': 'weather:read',
|
|
'categories.list': 'db:read:categories',
|
|
'tags.list': 'db:read:tags',
|
|
'tags.create': 'db:write:tags',
|
|
'tags.update': 'db:write:tags',
|
|
'tags.delete': 'db:write:tags',
|
|
'trips.members': 'db:read:trips',
|
|
'todos.list': 'db:read:todos',
|
|
'todos.create': 'db:write:todos',
|
|
'todos.update': 'db:write:todos',
|
|
'todos.delete': 'db:write:todos',
|
|
'costs.getByTrip': 'db:read:costs',
|
|
'costs.listMine': 'db:read:costs',
|
|
'costs.create': 'db:write:costs',
|
|
'costs.update': 'db:write:costs',
|
|
'costs.delete': 'db:write:costs',
|
|
'places.create': 'db:write:places',
|
|
'places.update': 'db:write:places',
|
|
'places.delete': 'db:write:places',
|
|
'days.create': 'db:write:days',
|
|
'days.update': 'db:write:days',
|
|
'days.delete': 'db:write:days',
|
|
'itinerary.assign': 'db:write:itinerary',
|
|
'itinerary.unassign': 'db:write:itinerary',
|
|
'trips.update': 'db:write:trips',
|
|
'meta.get': 'db:meta',
|
|
'meta.set': 'db:meta',
|
|
'meta.list': 'db:meta',
|
|
'meta.delete': 'db:meta',
|
|
'users.getById': 'db:read:users',
|
|
'ws.broadcastToTrip': 'ws:broadcast:trip',
|
|
'ws.broadcastToUser': 'ws:broadcast:user',
|
|
'notify.send': 'notify:send',
|
|
'ai.complete': 'ai:invoke',
|
|
'ai.extract': 'ai:invoke',
|
|
'oauth.getToken': 'oauth:client',
|
|
// Scheduling a userless future callback is the same risk class as a cron job, so
|
|
// it rides on the existing jobs:run grant (no new permission, no re-consent).
|
|
'scheduler.set': 'jobs:run',
|
|
'scheduler.cancel': 'jobs:run',
|
|
};
|
|
|
|
/** All permission strings the host understands (unknown ones are rejected at activation). */
|
|
export const KNOWN_PERMISSIONS = [
|
|
'db:own',
|
|
'db:read:trips',
|
|
'db:read:users',
|
|
'db:read:costs',
|
|
'db:read:packing',
|
|
'db:write:packing',
|
|
'db:read:files',
|
|
'db:read:files:content',
|
|
'db:write:files',
|
|
'db:read:collab',
|
|
'db:write:collab',
|
|
'db:write:members',
|
|
'db:create:trips',
|
|
'db:read:journal',
|
|
'db:read:atlas',
|
|
'rates:read',
|
|
'db:read:vacay',
|
|
'db:read:daynotes',
|
|
'db:read:collections',
|
|
'db:write:collections',
|
|
'db:write:atlas',
|
|
'db:write:vacay',
|
|
'db:write:journal',
|
|
'db:read:categories',
|
|
'db:read:tags',
|
|
'db:write:tags',
|
|
'db:read:todos',
|
|
'db:write:todos',
|
|
'weather:read',
|
|
'db:write:daynotes',
|
|
'db:write:costs',
|
|
'db:write:places',
|
|
'db:write:days',
|
|
'db:write:itinerary',
|
|
'db:write:trips',
|
|
'db:write:reservations',
|
|
'db:write:accommodations',
|
|
'db:meta',
|
|
'ws:broadcast:trip',
|
|
'ws:broadcast:user',
|
|
'hook:photo-provider',
|
|
'hook:calendar-source',
|
|
'hook:place-detail-provider',
|
|
'hook:trip-warning-provider',
|
|
'hook:table-contributor',
|
|
'hook:map-marker-provider',
|
|
'hook:map-layer-provider',
|
|
'hook:route-provider',
|
|
'hook:day-schedule-provider',
|
|
'hook:day-tint-provider',
|
|
'hook:pdf-section-provider',
|
|
'hook:atlas-layer-provider',
|
|
'hook:journal-entry-provider',
|
|
'hook:trip-card-provider',
|
|
'hook:notification-channel',
|
|
// Data-subject-rights hook: the host calls the plugin's deleteUserData /
|
|
// exportUserData when a TREK account is erased or its data is exported. Userless
|
|
// (the plugin only receives a userId and acts on its OWN db), so it grants no
|
|
// read into core data — it exists so a plugin can honour GDPR erasure/portability.
|
|
'hook:user-data',
|
|
'events:subscribe',
|
|
'jobs:run',
|
|
'http:outbound',
|
|
'notify:send',
|
|
'ai:invoke',
|
|
'oauth:client',
|
|
// Bridge-level permission (no RPC method): the plugin's sandboxed frames may ask
|
|
// the HOST for the browser's geolocation over postMessage. The host reads the
|
|
// position and posts plain data into the frame — the sandbox itself never gains
|
|
// the geolocation API, and the browser's own site permission prompt still
|
|
// applies. Nothing is sent to the server; the plugin's server code never sees a
|
|
// position unless its own client ships it through one of its routes.
|
|
'geolocation:read',
|
|
] as const;
|
|
|
|
export function isKnownPermission(p: string): boolean {
|
|
return (KNOWN_PERMISSIONS as readonly string[]).includes(p) || p.startsWith('http:outbound:');
|
|
}
|