@volter/twin-calcom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +90 -0
- package/client/calcom-mirror.css +29 -0
- package/client/calcom-mirror.tsx +125 -0
- package/dist/client/calcom-mirror.bundle.js +321 -0
- package/dist/client/calcom-mirror.css +29 -0
- package/dist/client/calcom-mirror.d.ts +13 -0
- package/dist/client/calcom-mirror.js +61 -0
- package/dist/client/calcom-mirror.tsx +125 -0
- package/dist/src/calcom-budget.d.ts +52 -0
- package/dist/src/calcom-budget.js +145 -0
- package/dist/src/calcom-capabilities.d.ts +3 -0
- package/dist/src/calcom-capabilities.js +521 -0
- package/dist/src/calcom-conformance.d.ts +15 -0
- package/dist/src/calcom-conformance.js +86 -0
- package/dist/src/calcom-connector.d.ts +90 -0
- package/dist/src/calcom-connector.js +229 -0
- package/dist/src/calcom-mirror-ui.d.ts +23 -0
- package/dist/src/calcom-mirror-ui.js +100 -0
- package/dist/src/calcom-perform-harness.d.ts +5 -0
- package/dist/src/calcom-perform-harness.js +29 -0
- package/dist/src/calcom-server.d.ts +14 -0
- package/dist/src/calcom-server.js +31 -0
- package/dist/src/calcom-twin.d.ts +16 -0
- package/dist/src/calcom-twin.js +718 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +31 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +69 -0
- package/package.json +72 -0
- package/src/calcom-budget.ts +171 -0
- package/src/calcom-capabilities.ts +529 -0
- package/src/calcom-conformance.ts +92 -0
- package/src/calcom-connector.ts +258 -0
- package/src/calcom-mirror-ui.ts +110 -0
- package/src/calcom-perform-harness.ts +24 -0
- package/src/calcom-server.ts +39 -0
- package/src/calcom-twin.ts +675 -0
- package/src/cli.ts +29 -0
- package/src/index.ts +101 -0
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
|
+
// world-calcom CLI: serve the Cal.com v2 API twin, the mirror UI, or run conformance.
|
|
4
|
+
import { hasFlag, optionValue } from '@volter/world-core/args';
|
|
5
|
+
import { createCalcomTwinServer } from "./calcom-server.js";
|
|
6
|
+
import { createCalcomMirrorServer } from "./calcom-mirror-ui.js";
|
|
7
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
8
|
+
const port = Number(optionValue(rest, '--port', '0')) || undefined;
|
|
9
|
+
const root = optionValue(rest, '--root') || undefined;
|
|
10
|
+
const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
|
|
11
|
+
if (cmd === 'serve') {
|
|
12
|
+
const s = await createCalcomTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
13
|
+
process.stdout.write(`calcom twin (v2 API)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
|
|
14
|
+
await keepProcessAlive();
|
|
15
|
+
}
|
|
16
|
+
else if (cmd === 'mirror') {
|
|
17
|
+
const s = await createCalcomMirrorServer({ ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
18
|
+
process.stdout.write(`calcom mirror UI (dashboard) at http://127.0.0.1:${s.port}\n`);
|
|
19
|
+
await keepProcessAlive();
|
|
20
|
+
}
|
|
21
|
+
else if (cmd === 'conformance') {
|
|
22
|
+
// dev-only; lazy so the bin runs without @volter/world-tooling in the runtime graph
|
|
23
|
+
const { checkCalcomConformance } = await import("./calcom-conformance.js");
|
|
24
|
+
const report = checkCalcomConformance({ ...(root ? { root } : {}) });
|
|
25
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
26
|
+
if (!report.ok)
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.stdout.write('Usage: world-calcom serve|mirror|conformance [--port N] [--root DIR] [--read-only]\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { handleCalcomTwinRequest, TWIN_API_VERSION, RESOURCE_TYPES } from './calcom-twin.js';
|
|
2
|
+
export type { CalcomRequest, CalcomResponse } from './calcom-twin.js';
|
|
3
|
+
export { createCalcomTwinFetch, createCalcomTwinServer, type CalcomTwinFetchOptions } from './calcom-server.js';
|
|
4
|
+
export { calcomRequestForAction, liveCalcomExecute, mapBooking, mapEventType, pullCalcomAll, pullCalcomBookings, pullCalcomEventTypes, syncCalcomFromReal, } from './calcom-connector.js';
|
|
5
|
+
export type { CalcomExecute, LiveCalcomOptions } from './calcom-connector.js';
|
|
6
|
+
export { CALCOM_BUDGET_CEILING, CALCOM_BUDGET_MAX_RETRY_AFTER_S, CALCOM_BUDGET_WINDOW_MS, CALCOM_CALL_WEIGHTS, CALCOM_RATE_BUDGET, CalcomBudget, CalcomBudgetError, calcomBudgetPath, calcomCallWeight, } from './calcom-budget.js';
|
|
7
|
+
export type { CalcomBudgetErrorKind, CalcomBudgetOptions, CalcomBudgetReservation, CalcomBudgetSnapshot } from './calcom-budget.js';
|
|
8
|
+
export { attendeeLabel, bookingTone, buildCalcomMirrorClient, calcomMirrorHtml, createCalcomMirrorServer, formatWindow, } from './calcom-mirror-ui.js';
|
|
9
|
+
export type { CalcomRow } from './calcom-mirror-ui.js';
|
|
10
|
+
import { type TwinPack } from '@volter/world-core';
|
|
11
|
+
export declare const pack: TwinPack;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @volter/twin-calcom — the Cal.com twin (one vendor, one package), built on the shared
|
|
2
|
+
// @volter/world-core kernel. REST (v2) transport, spec-faithful `{ status, data }` envelope,
|
|
3
|
+
// stateful bookings/event-types/schedules/teams/webhooks, and a React dashboard mirror.
|
|
4
|
+
// Auth is a faked-locally Bearer API key — there is NO JWT / JWKS / webhook signing
|
|
5
|
+
// (Cal.com's API auth is a plain bearer `cal_…` key). Conformance tooling lives in
|
|
6
|
+
// @volter/world-tooling (a dev dependency) — not shipped in the runtime API.
|
|
7
|
+
export { handleCalcomTwinRequest, TWIN_API_VERSION, RESOURCE_TYPES } from "./calcom-twin.js";
|
|
8
|
+
export { createCalcomTwinFetch, createCalcomTwinServer } from "./calcom-server.js";
|
|
9
|
+
export { calcomRequestForAction, liveCalcomExecute, mapBooking, mapEventType, pullCalcomAll, pullCalcomBookings, pullCalcomEventTypes, syncCalcomFromReal, } from "./calcom-connector.js";
|
|
10
|
+
// The client-side rate budget — the fail-closed backstop `liveCalcomExecute` routes every live
|
|
11
|
+
// request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
|
|
12
|
+
// here is Cal.com's DECLARATION (window/ceiling/per-endpoint weights) plus the vendor-bound
|
|
13
|
+
// bindings. Exported so an operator can inspect spend (`snapshot`) and so a caller can catch
|
|
14
|
+
// `CalcomBudgetError` by type; there is deliberately no export that disables the guard.
|
|
15
|
+
export { CALCOM_BUDGET_CEILING, CALCOM_BUDGET_MAX_RETRY_AFTER_S, CALCOM_BUDGET_WINDOW_MS, CALCOM_CALL_WEIGHTS, CALCOM_RATE_BUDGET, CalcomBudget, CalcomBudgetError, calcomBudgetPath, calcomCallWeight, } from "./calcom-budget.js";
|
|
16
|
+
export { attendeeLabel, bookingTone, buildCalcomMirrorClient, calcomMirrorHtml, createCalcomMirrorServer, formatWindow, } from "./calcom-mirror-ui.js";
|
|
17
|
+
// Registry descriptor: the pack self-describes so tooling can discover it.
|
|
18
|
+
import { registerPack } from '@volter/world-core';
|
|
19
|
+
import { CALCOM_RATE_BUDGET as RATE_BUDGET } from "./calcom-budget.js";
|
|
20
|
+
import { performCalcomAction, syncCalcomFromRemote } from "./calcom-connector.js";
|
|
21
|
+
export const pack = {
|
|
22
|
+
vendor: 'calcom',
|
|
23
|
+
// PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of the real
|
|
24
|
+
// state system: perform one entry against Cal.com v2, refresh the root from it. Moved 2026-09-08.
|
|
25
|
+
protocol: '2',
|
|
26
|
+
refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
|
|
27
|
+
stateSystem: { perform: performCalcomAction, refresh: syncCalcomFromRemote },
|
|
28
|
+
// the round trip: an event type, then a booking of it — the two things a scheduling account is made of
|
|
29
|
+
// the round trip is ONE write — an event type. A booking would have to name an event type, and the only
|
|
30
|
+
// one it could name statically is the twin's implicit default (7001), which is not the subject the kernel
|
|
31
|
+
// adopts; the reference trip below is where a booking gets made, against the event type this created.
|
|
32
|
+
roundTrip: { method: 'POST', path: '/v2/event-types', body: { title: 'Round trip', slug: 'round-trip', lengthInMinutes: 30 }, headers: { authorization: 'Bearer round-trip', 'cal-api-version': '2024-06-14' } },
|
|
33
|
+
// rule 5: a booking names the event type it is a booking OF; when the head adopts Cal.com's id for the
|
|
34
|
+
// event type, the kernel resolves the booking's field first
|
|
35
|
+
referenceTrip: { method: 'POST', path: '/v2/bookings', body: { start: '2026-09-09T11:00:00.000Z', eventTypeId: '{{evtId}}', attendee: { name: 'Round Trip', email: 'round@trip.test', timeZone: 'UTC' } }, headers: { authorization: 'Bearer round-trip', 'cal-api-version': '2024-08-13' } },
|
|
36
|
+
references: [{
|
|
37
|
+
type: 'booking', to: 'event_type',
|
|
38
|
+
key: (f) => (f.eventTypeId === undefined || f.eventTypeId === null ? undefined : `evt_${String(f.eventTypeId)}`),
|
|
39
|
+
adopt: (_f, vendorId) => ({ eventTypeId: vendorId.replace(/^evt_/, '') }),
|
|
40
|
+
}],
|
|
41
|
+
parityOrigin: 'http://twin',
|
|
42
|
+
shapeParity: 'held',
|
|
43
|
+
// The SAME object calcom-budget.ts declares at module load — one source of truth, so registering
|
|
44
|
+
// the pack and importing the connector can never arm two different ceilings.
|
|
45
|
+
rateBudget: RATE_BUDGET,
|
|
46
|
+
transport: 'rest',
|
|
47
|
+
archetype: 'crud',
|
|
48
|
+
bin: 'world-calcom',
|
|
49
|
+
resources: ['booking', 'event_type', 'schedule', 'team', 'membership', 'webhook', 'slot'],
|
|
50
|
+
specSource: 'cal.com/docs/api-reference/v2 (hand-authored from the published v2 reference)',
|
|
51
|
+
description: 'Cal.com v2 REST twin — { status, data } envelope, stateful bookings/event-types/schedules/teams/webhooks, dashboard mirror.',
|
|
52
|
+
// Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
53
|
+
// 2026-08-31). Both credential-env spellings the vendor uses — CALCOM_* and the
|
|
54
|
+
// shorter CAL_* — stem here; there is no npm client to claim (Cal.com integrations call the v2
|
|
55
|
+
// REST surface directly), so `sdks` is deliberately absent rather than invented.
|
|
56
|
+
adoption: {
|
|
57
|
+
// Cal.com ships no Python SDK and no community client exists. The `Calcom` distribution on
|
|
58
|
+
// PyPI is an unrelated mathematics package - claiming it would map a maths dependency onto a
|
|
59
|
+
// scheduling twin.
|
|
60
|
+
pypi: [],
|
|
61
|
+
envStems: ['CALCOM', 'CAL'],
|
|
62
|
+
},
|
|
63
|
+
hosts: [{ host: 'api.cal.com' }],
|
|
64
|
+
// A scheduling client calls api.cal.com/v2/… — the dev proxy forwards '/v2/' to the twin
|
|
65
|
+
// and strips the absolute host so calls come back same-origin.
|
|
66
|
+
browserRouting: { apiPathPrefix: '/v2/', loaderHost: 'https://api.cal.com' },
|
|
67
|
+
};
|
|
68
|
+
// registered at import: the kernel learns the pack's state system (protocol 2)
|
|
69
|
+
registerPack(pack);
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@volter/twin-calcom",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local Cal.com twin — a faithful, stateful local Cal.com v2 API your scheduling integration talks to unmodified. Mirror, simulate, and fork. Built on @volter/world-core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"twin",
|
|
7
|
+
"local",
|
|
8
|
+
"mock",
|
|
9
|
+
"mirror",
|
|
10
|
+
"simulator",
|
|
11
|
+
"fixtures",
|
|
12
|
+
"testing",
|
|
13
|
+
"api",
|
|
14
|
+
"scheduling",
|
|
15
|
+
"calcom",
|
|
16
|
+
"cal.com",
|
|
17
|
+
"bookings"
|
|
18
|
+
],
|
|
19
|
+
"author": "Volter (https://github.com/volter-ai)",
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"client",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"!**/*.test.ts",
|
|
27
|
+
"!**/*.test.tsx",
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/volter-ai/twin.git",
|
|
33
|
+
"directory": "packages/twin/calcom"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/calcom#readme",
|
|
36
|
+
"type": "module",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/src/index.d.ts",
|
|
40
|
+
"default": "./dist/src/index.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"bin": {
|
|
44
|
+
"world-calcom": "dist/src/cli.js"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "bun test src/*.test.ts",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"build": "node ../../../scripts/publish/build.mjs",
|
|
50
|
+
"prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
|
|
51
|
+
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"react": "^19.2.7",
|
|
55
|
+
"react-dom": "^19.2.7"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@volter/world-core": "2.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@volter/world-core": "2.0.0",
|
|
62
|
+
"@volter/world-tooling": "0.1.0",
|
|
63
|
+
"@types/bun": "^1.2.20",
|
|
64
|
+
"@types/node": "^24.0.0",
|
|
65
|
+
"@types/react": "^19.2.17",
|
|
66
|
+
"@types/react-dom": "^19.2.3",
|
|
67
|
+
"typescript": "^5.9.0"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=22.3"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Cal.com's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus the thin typed
|
|
2
|
+
// bindings `liveCalcomExecute` uses. The MECHANISM — the durable token-keyed ledger, the rolling
|
|
3
|
+
// window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a corrupt ledger —
|
|
4
|
+
// lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`). Read that module's
|
|
5
|
+
// header for the full rationale AND for the honest list of what the guard does not guarantee (an
|
|
6
|
+
// injected clock or ledger path still defeats it — it guards carelessness, not malice).
|
|
7
|
+
//
|
|
8
|
+
// ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// A real ~4.5-DAY vendor lockout (Figma, 2026-07-25) happened because raw API calls were made
|
|
10
|
+
// outside the pack's connector — no cache, no batching, no ceiling. Discipline only binds the code
|
|
11
|
+
// that follows it; a BUDGET binds the code that does not.
|
|
12
|
+
//
|
|
13
|
+
// ── HOW THE CEILING WAS CHOSEN ──────────────────────────────────────────────────────────────
|
|
14
|
+
// Cal.com DOES publish a scalar limit, so this models the real thing rather than guessing. From
|
|
15
|
+
// https://cal.com/docs/api-reference/v2/introduction (read 2026-07-26): API-key authentication is
|
|
16
|
+
// limited to 120 requests per minute (raisable on request); the same 120/minute is the default for
|
|
17
|
+
// unauthenticated traffic.
|
|
18
|
+
//
|
|
19
|
+
// The `X-RateLimit-Limit` / `-Remaining` / `-Reset` response headers this guard reads are NOT on that
|
|
20
|
+
// page — they are the conventional spelling, matched structurally by the kernel, and are read IF
|
|
21
|
+
// PRESENT and never assumed. (§9 corrected an earlier line here that attributed them to the cited
|
|
22
|
+
// page, 2026-07-26. Do not turn an observation into a citation.)
|
|
23
|
+
//
|
|
24
|
+
// The ceiling is 60 weighted units per 60s — 60 calls a minute at the default weight, HALF the
|
|
25
|
+
// documented 120/minute, so the 60-second AVERAGE stays under the vendor's own even before any
|
|
26
|
+
// weighting. It is more permissive than the kernel's undeclared fallback (30 calls/min) precisely
|
|
27
|
+
// because 120/min is documented; without that number it would not be.
|
|
28
|
+
//
|
|
29
|
+
// It bounds the average; it does NOT pace (the kernel refuses, it never sleeps — see its header).
|
|
30
|
+
// Cal.com's window is also a minute, so the shapes line up well here, but an intra-second burst can
|
|
31
|
+
// still reach the vendor first; the backstop for that is the cooldown armed from the 429 /
|
|
32
|
+
// `X-RateLimit-Remaining: 0`.
|
|
33
|
+
//
|
|
34
|
+
// ── HOW THE WEIGHTS WERE CHOSEN (and what is a judgement call) ───────────────────────────────
|
|
35
|
+
// Cal.com counts REQUESTS, so weight 1 is the faithful price and that is what reads get. Two
|
|
36
|
+
// deliberate exceptions, both judgement calls rather than published costs:
|
|
37
|
+
// • `POST /bookings` costs 3. A booking is not a database row — it sends invitation e-mail,
|
|
38
|
+
// writes to the organiser's and attendees' real connected calendars, and fires webhooks. A
|
|
39
|
+
// runaway create loop is the failure mode with a HUMAN blast radius, not just a throttle.
|
|
40
|
+
// • `GET /slots` costs 2. Availability is computed across every connected calendar for a date
|
|
41
|
+
// range, so it is the expensive read, and it is the one a naive UI polls in a loop.
|
|
42
|
+
import {
|
|
43
|
+
declareRateBudget,
|
|
44
|
+
rateBudgetPath,
|
|
45
|
+
rateBudgetWeight,
|
|
46
|
+
RateBudget,
|
|
47
|
+
type RateBudgetDeclaration,
|
|
48
|
+
type RateBudgetOptions,
|
|
49
|
+
type RateBudgetReservation,
|
|
50
|
+
type RateBudgetSnapshot,
|
|
51
|
+
} from '@volter/world-core';
|
|
52
|
+
|
|
53
|
+
const VENDOR = 'calcom';
|
|
54
|
+
|
|
55
|
+
/** Rolling window, in ms. Spend older than this is pruned. */
|
|
56
|
+
export const CALCOM_BUDGET_WINDOW_MS = 60_000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Weighted units allowed inside one window. 60/60s = 60 calls a minute at the default weight —
|
|
60
|
+
* half Cal.com's documented 120 requests/minute per API key.
|
|
61
|
+
*/
|
|
62
|
+
export const CALCOM_BUDGET_CEILING = 60;
|
|
63
|
+
|
|
64
|
+
/** Seconds. A `Retry-After` above this means the key is throttled hard — fail loudly, don't sleep. */
|
|
65
|
+
export const CALCOM_BUDGET_MAX_RETRY_AFTER_S = 300;
|
|
66
|
+
|
|
67
|
+
/** Per-call cost, keyed by `"<METHOD> <path>"`. See the header for what is documented vs. judged. */
|
|
68
|
+
export const CALCOM_CALL_WEIGHTS = {
|
|
69
|
+
/** `POST /bookings` — sends e-mail, writes real calendars, fires webhooks. Human blast radius. */
|
|
70
|
+
book: 3,
|
|
71
|
+
/** `GET /slots` — availability computed across every connected calendar. The expensive read. */
|
|
72
|
+
slots: 2,
|
|
73
|
+
/** Everything else: booking/event-type reads, updates, cancellations, schedules, me. */
|
|
74
|
+
other: 1,
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
/** THE PACK'S DECLARATION — pure data, the only Cal.com-specific thing in the whole budget. */
|
|
78
|
+
export const CALCOM_RATE_BUDGET: RateBudgetDeclaration = {
|
|
79
|
+
windowMs: CALCOM_BUDGET_WINDOW_MS,
|
|
80
|
+
ceiling: CALCOM_BUDGET_CEILING,
|
|
81
|
+
defaultWeight: CALCOM_CALL_WEIGHTS.other,
|
|
82
|
+
maxRetryAfterSeconds: CALCOM_BUDGET_MAX_RETRY_AFTER_S,
|
|
83
|
+
rules: [
|
|
84
|
+
{ match: '^POST /bookings$', weight: CALCOM_CALL_WEIGHTS.book },
|
|
85
|
+
{ match: '^GET /slots', weight: CALCOM_CALL_WEIGHTS.slots },
|
|
86
|
+
],
|
|
87
|
+
reason:
|
|
88
|
+
'Cal.com documents 120 requests/minute per API key (cal.com/docs/api-reference/v2/introduction, ' +
|
|
89
|
+
'read 2026-07-26; the same 120/min is the unauthenticated default, and it is raisable on ' +
|
|
90
|
+
'request). That page documents no response headers; the X-RateLimit-* names the guard reads are ' +
|
|
91
|
+
'the conventional spelling, read if present and never assumed. 60 weighted units / 60s is 60 ' +
|
|
92
|
+
'calls a minute — HALF the documented 120, so the 60s average stays ' +
|
|
93
|
+
"under the vendor's own. It is deliberately more permissive than the kernel's undeclared " +
|
|
94
|
+
'fallback (30 calls/min) BECAUSE 120/min is documented; without that number it would not be. ' +
|
|
95
|
+
'POST /bookings costs 3 (it sends e-mail, writes real connected calendars and fires webhooks — ' +
|
|
96
|
+
'a human blast radius, not just a throttle) and GET /slots costs 2 (availability computed across ' +
|
|
97
|
+
'every connected calendar, and the endpoint a naive UI polls in a loop) — judgement calls, not ' +
|
|
98
|
+
'published costs. The window bounds the 60s AVERAGE and does not pace; the 429 / ' +
|
|
99
|
+
'`X-RateLimit-Remaining: 0` cooldown is the backstop for a sub-second burst.',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Declared at module load, so merely importing this module (which `calcom-connector.ts` does) is
|
|
103
|
+
// enough to arm the real ceiling. `RateBudget` reads its policy live precisely so this declaration
|
|
104
|
+
// takes effect the moment it lands, and constructing through the subclass below (which imports this
|
|
105
|
+
// module) is what makes the ordering a non-issue in practice.
|
|
106
|
+
declareRateBudget(VENDOR, CALCOM_RATE_BUDGET);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Price one call. The key is `"<METHOD> <path>"` (the v2 path as the connector states it, without
|
|
110
|
+
* the `/v2` prefix the live executor adds) with the query string split off, so a rule can price by
|
|
111
|
+
* method without the kernel knowing anything about Cal.com. An unclassified endpoint still costs
|
|
112
|
+
* `defaultWeight` — nothing is ever free.
|
|
113
|
+
*/
|
|
114
|
+
export function calcomCallWeight(method: string, path: string): number {
|
|
115
|
+
const { bare, query } = splitQuery(path);
|
|
116
|
+
// UPPER-CASE the method: `fetch` normalizes a known lowercase method before sending, so
|
|
117
|
+
// `execute('post', …)` really does issue a POST and must be priced as one.
|
|
118
|
+
return rateBudgetWeight(VENDOR, `${String(method).toUpperCase()} ${bare}`, query);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `/v1/x?a=1` -> `{ bare: '/v1/x', query: { a: '1' } }`. Rules match the path; `whenQuery*` the query.
|
|
123
|
+
*
|
|
124
|
+
* NORMALIZED, because the anchored rules are otherwise trivially evaded (§9 finding, 2026-07-26):
|
|
125
|
+
* `fetch` upper-cases a known method before sending, so `execute('post', …)` issues a real WRITE
|
|
126
|
+
* that a `^POST ` rule would price as a read; and a trailing slash makes a path miss a `$` anchor
|
|
127
|
+
* while most routers treat it as the same endpoint. Both are input variations, not attacks, and
|
|
128
|
+
* either one silently voids the "expensive endpoints are priced up" claim the ceiling rests on.
|
|
129
|
+
*/
|
|
130
|
+
function splitQuery(path: string): { bare: string; query: Record<string, string> } {
|
|
131
|
+
const at = path.indexOf('?');
|
|
132
|
+
const query: Record<string, string> = {};
|
|
133
|
+
if (at !== -1) for (const [k, v] of new URLSearchParams(path.slice(at + 1))) query[k] = v;
|
|
134
|
+
const raw = at === -1 ? path : path.slice(0, at);
|
|
135
|
+
// Collapse a trailing slash, but never turn the root path into the empty string.
|
|
136
|
+
const bare = raw.length > 1 && raw.endsWith('/') ? raw.replace(/\/+$/, '') : raw;
|
|
137
|
+
return { bare, query };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Where Cal.com's ledger lives. Token-keyed and cwd-independent by default (the limit is per API
|
|
141
|
+
* key, so a cwd-scoped ledger would hand the same key a fresh allowance in every checkout,
|
|
142
|
+
* worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
|
|
143
|
+
export function calcomBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
|
|
144
|
+
const o = typeof opts === 'string' ? { root: opts } : opts;
|
|
145
|
+
// VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
|
|
146
|
+
// excess-property check only catches object literals) must not redirect this pack's ledger to
|
|
147
|
+
// another vendor's file.
|
|
148
|
+
return rateBudgetPath({ ...o, vendor: VENDOR });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Construction options for Cal.com's budget. The vendor is fixed; everything else may only TIGHTEN. */
|
|
152
|
+
export type CalcomBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Cal.com's budget — the shared kernel guard bound to this vendor's declaration. A real subclass,
|
|
156
|
+
* not an alias, so `budget instanceof CalcomBudget` in `liveCalcomExecute` means "a budget that
|
|
157
|
+
* accounts against CAL.COM's ledger under CAL.COM's ceiling": another vendor's `RateBudget` (with
|
|
158
|
+
* its own, possibly larger, ceiling) is NOT assignable there.
|
|
159
|
+
*/
|
|
160
|
+
export class CalcomBudget extends RateBudget {
|
|
161
|
+
constructor(opts: CalcomBudgetOptions = {}) {
|
|
162
|
+
super({ ...opts, vendor: VENDOR });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The typed refusal. One error class shared with every other vendor's budget; `err.vendor` says
|
|
167
|
+
* which one refused, and `err.kind` says why. */
|
|
168
|
+
export { RateBudgetError as CalcomBudgetError } from '@volter/world-core';
|
|
169
|
+
export type { RateBudgetErrorKind as CalcomBudgetErrorKind } from '@volter/world-core';
|
|
170
|
+
export type CalcomBudgetReservation = RateBudgetReservation;
|
|
171
|
+
export type CalcomBudgetSnapshot = RateBudgetSnapshot;
|