@odla-ai/cli 0.11.2 → 0.13.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.js CHANGED
@@ -2,19 +2,19 @@
2
2
  import {
3
3
  AGENT_HARNESSES,
4
4
  CAPABILITIES,
5
- GOOGLE_CALENDAR_READ_SCOPE,
5
+ GOOGLE_CALENDAR_EVENTS_SCOPE,
6
6
  SYSTEM_AI_PURPOSES,
7
7
  adminAi,
8
8
  calendarBookingPageUrl,
9
9
  calendarCalendars,
10
10
  calendarConnect,
11
11
  calendarDisconnect,
12
- calendarResync,
13
12
  calendarServiceConfig,
14
13
  calendarStatus,
15
14
  connectGitHubSecuritySource,
16
15
  disconnectGitHubSecuritySource,
17
16
  doctor,
17
+ exitCodeFor,
18
18
  followHostedSecurityJob,
19
19
  getHostedSecurityIntent,
20
20
  getHostedSecurityJob,
@@ -38,23 +38,23 @@ import {
38
38
  secretsSetClerkKey,
39
39
  smoke,
40
40
  startHostedSecurityJob
41
- } from "./chunk-DC4MIB4X.js";
41
+ } from "./chunk-I7XDJZB6.js";
42
42
  export {
43
43
  AGENT_HARNESSES,
44
44
  CAPABILITIES,
45
- GOOGLE_CALENDAR_READ_SCOPE,
45
+ GOOGLE_CALENDAR_EVENTS_SCOPE,
46
46
  SYSTEM_AI_PURPOSES,
47
47
  adminAi,
48
48
  calendarBookingPageUrl,
49
49
  calendarCalendars,
50
50
  calendarConnect,
51
51
  calendarDisconnect,
52
- calendarResync,
53
52
  calendarServiceConfig,
54
53
  calendarStatus,
55
54
  connectGitHubSecuritySource,
56
55
  disconnectGitHubSecuritySource,
57
56
  doctor,
57
+ exitCodeFor,
58
58
  followHostedSecurityJob,
59
59
  getHostedSecurityIntent,
60
60
  getHostedSecurityJob,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odla-ai/cli",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "description": "Agent-operable CLI for odla provisioning, calendar consent and sync lifecycle, System AI administration, Worker secrets, security jobs, and smoke checks.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://odla.ai/docs/packages/cli",
@@ -2,7 +2,7 @@
2
2
  name: odla
3
3
  description: >
4
4
  Build a new app on odla — a realtime database (odla-db) plus AI, Clerk auth,
5
- read-only Google Calendar mirrors, and observability on Cloudflare, provisioned by @odla-ai/cli. Use when the user
5
+ live Google Calendar booking, and observability on Cloudflare, provisioned by @odla-ai/cli. Use when the user
6
6
  wants to create/scaffold an odla app, add odla-db/ai/auth/o11y to a repo, or
7
7
  "get started with odla". For moving an existing static or GitHub Pages site to
8
8
  odla, use the odla-migrate skill instead.
@@ -15,7 +15,7 @@ runbookOrder:
15
15
 
16
16
  You drive setup and building on **odla** — an agent-operable app platform on
17
17
  Cloudflare: a realtime graph database (**odla-db**), multi-provider **AI**,
18
- **Clerk** auth, read-only **Google Calendar** mirrors, and OpenTelemetry **observability**, all provisioned by the
18
+ **Clerk** auth, live **Google Calendar** booking, and OpenTelemetry **observability**, all provisioned by the
19
19
  `@odla-ai/cli`. The human approves a couple of browser-only checkpoints; you do
20
20
  everything else.
21
21
 
@@ -53,7 +53,8 @@ State which path you're taking and what you'll build in one line; get a nod.
53
53
  explicitly asks.
54
54
  5. Google OAuth is a distinct human checkpoint. Never request, paste, print, or
55
55
  store a Google authorization code/refresh token. Open only the state-bound
56
- platform/Google consent URL returned by odla; calendar actions remain unavailable in the read-only slice.
56
+ platform/Google consent URL returned by odla; booking runs server-side
57
+ through the SDK with the app's existing key, never from a browser.
57
58
 
58
59
  ## Tooling sources
59
60
 
@@ -94,12 +95,19 @@ guessing which platform/credential steps need manual work. Then:
94
95
  `npx @odla-ai/cli provision --email <existing-odla-account>
95
96
  --write-dev-vars --push-secrets`. A device code prints; that same account
96
97
  signs in, reviews the exact code, and approves it at https://odla.ai/studio.
97
- Opening the link alone is inert. It creates the app,
98
+ Opening the link alone is inert. Relay the printed approval URL to the
99
+ human verbatim — the automatic browser launch is best-effort and may show
100
+ no tab. Outside an interactive terminal the wait is capped (90s default,
101
+ `--wait <seconds>`); exit code 75 means still pending: the handshake is
102
+ persisted, so once the human approves, re-run the same command and it
103
+ resumes the same code and collects the token. It creates the app,
98
104
  enables services, issues or reuses configured credentials (db key + o11y
99
- ingest token when enabled), pushes schema + rules, writes `.dev.vars`, and
105
+ ingest token when enabled), composes declared integration schema/rules and
106
+ guarded seeds, writes `.dev.vars`, and
100
107
  transfers Worker secrets through Wrangler stdin. With calendar enabled it
101
- then prints a second, server-issued Google URL; the human grants read-only consent
102
- and the CLI follows initial sync.
108
+ then prints a second, server-issued Google URL; the human grants booking
109
+ consent in a browser and the connection is immediately live — nothing
110
+ syncs.
103
111
  4. **run** — `npx wrangler dev` (auto-loads `.dev.vars`); verify locally.
104
112
  5. **security** — run the passive `@odla-ai/security` odla profile; inspect
105
113
  every lead and keep critical candidate gating enabled. If the human approves
@@ -120,14 +128,16 @@ guessing which platform/credential steps need manual work. Then:
120
128
 
121
129
  `npx @odla-ai/cli doctor` is an offline config check anytime;
122
130
  `npx @odla-ai/cli smoke --env dev` verifies a live deployment.
123
- Use `calendar status --json` for safe connection/sync state and `calendar
124
- calendars --json` to discover ids after consent; update checked-in config and
125
- re-provision. Resync production only with `--yes`; disconnect always requires
126
- `--yes` and leaves retained rows potentially stale.
131
+ Use `calendar status --json` for safe connection state (`bookable`, booking
132
+ and availability calendars) and `calendar calendars --json` to discover ids
133
+ after consent; update checked-in config and re-provision. Disconnect always
134
+ requires `--yes` and removes only this connection's sealed grant — Google
135
+ keeps every booked event.
127
136
 
128
137
  The CLI owns deterministic platform work from `odla.config.mjs`: service
129
- enablement, credential issuance/storage, `.dev.vars`, and Wrangler secret
130
- transfer. You own source changes — install `@odla-ai/o11y`, add
138
+ enablement, integration schema/rules/seeds, credential issuance/storage,
139
+ `.dev.vars`, and Wrangler secret transfer. You own source changes — install
140
+ runtime capability packages, mount their routes, supply authorization, add
131
141
  `withObservability`, create the Wrangler config, and choose application-specific
132
142
  signals. The human owns device approval, production consent, and explicit
133
143
  destructive rotation. If an
@@ -15,12 +15,18 @@ deny-all rules from the schema. To turn on auth/observability, add `"o11y"` to
15
15
  `services` and an `auth.clerk` block in `odla.config.mjs` (init leaves it
16
16
  commented).
17
17
 
18
- For a Google booking mirror, include both `"db"` and `"calendar"`. Configure
19
- `calendar.google.calendars` for every env (start with `"primary"`), optional
20
- match filters, and `attendeePolicy: "full" | "hashed"`. This release is
21
- read-only; do not add OAuth credentials or action scopes to config.
22
- For static Appointment Schedule parity, add the public per-env
23
- `bookingPageUrl`; it is link/embed configuration, never a credential.
18
+ For an npm app capability such as `@odla-ai/crm`, import its data-only
19
+ descriptor into `odla.config.mjs` and add it under `integrations`; do not add
20
+ the capability id to `services`. Provision collision-checks and merges its
21
+ schema/rules, creates guarded seeds only when absent, and leaves runtime package
22
+ installation and route mounting to the source step.
23
+
24
+ For Google Calendar booking, include both `"db"` and `"calendar"`. Configure
25
+ `calendar.google.availabilityCalendars` for every env (start with
26
+ `"primary"`; 1–10 per env) and optionally `bookingCalendar` (defaults to the
27
+ first availability calendar). Do not add OAuth credentials to config — the
28
+ Google grant is platform-side. A legacy per-env `bookingPageUrl` link may be
29
+ kept as a fallback; it is public configuration, never a credential.
24
30
 
25
31
  ## 2. Build the Worker shell
26
32
 
@@ -50,19 +56,22 @@ Prints a short device code and a link. ⏸ That same account opens the link, sig
50
56
  in, explicitly reviews the exact code, and approves it — loading the URL alone
51
57
  does not claim access, and no secret passes through the chat. Provision then:
52
58
  creates the app, enables its services, issues or reuses the db key (and the
53
- o11y ingest token when o11y is enabled), pushes schema + rules, writes
59
+ o11y ingest token when o11y is enabled), pushes the composed app + integration
60
+ schema/rules, creates missing guarded seeds, writes
54
61
  `.dev.vars`, and transfers configured Worker secrets through Wrangler stdin.
55
62
  Local credential files are `0600` and gitignored. Verify with
56
- `npx @odla-ai/cli doctor` — it prints the app, envs, services, and flags
63
+ `npx @odla-ai/cli doctor` — it prints the app, envs, services, integrations, and flags
57
64
  anything unset. `--push-secrets` preflights the Wrangler config and login before
58
65
  issuing or rotating a shown-once credential.
59
66
 
60
67
  Calendar adds a second ⏸ checkpoint after the odla device approval: provision
61
- prints/opens a state-bound Google URL issued by the platform and waits while the human grants
62
- `calendar.events.readonly` and the hosted connector performs initial sync. The
63
- CLI never receives the Google callback code or tokens. Once connected, run
68
+ prints/opens a state-bound Google URL issued by the platform and waits while
69
+ the human grants the booking scopes in a browser. The CLI never receives the
70
+ Google callback code or tokens, and nothing syncs afterward — Google stays
71
+ the single source of truth. Once connected, run
64
72
  `npx @odla-ai/cli calendar calendars --env dev --json`, refine the checked-in
65
- calendar ids if needed, re-provision, and verify `calendar status --json`.
73
+ calendar ids if needed, re-provision, and verify `calendar status --env dev`
74
+ reports `bookable: yes`.
66
75
 
67
76
  The CLI stops at the source boundary: it verifies but does not invent your
68
77
  application semantics. Do not use Studio to mint a routine o11y token. Manual
@@ -82,10 +91,11 @@ means the namespace has no rule yet — add one in `src/odla/rules.mjs`
82
91
  (deliberately; e.g. `{ notes: { view: "auth.signedIn", create: "auth.signedIn" } }`),
83
92
  re-provision to push it, and retry.
84
93
 
85
- Calendar uses the same `ODLA_ENDPOINT`, `ODLA_TENANT`, and server-only
86
- `ODLA_API_KEY`; it adds no Worker secret. Keep `initCalendar` in trusted Worker
87
- code. Browser code uses db subscriptions under explicit rules or pure
88
- `@odla-ai/calendar/client` helpers, never the admin key.
94
+ Calendar rides the server-only `ODLA_API_KEY`; it adds no Worker secret. Keep
95
+ `initCalendar` in trusted Worker code and expose the app's own slots/booking
96
+ endpoints to the browser never call the platform calendar routes from
97
+ browser code. Browsers may import only the pure `@odla-ai/calendar/client`
98
+ helpers (`computeBookableSlots`, formatters), never the admin key.
89
99
 
90
100
  ## 5. Security preflight
91
101
 
@@ -32,34 +32,46 @@ Worker/admin side: `init({ appId: tenantId, adminToken: env.ODLA_API_KEY, endpoi
32
32
  The publishable key is public; `provision` stores it (`setAuth`). Rules evaluate
33
33
  the signed-in user's JWT as `auth` — e.g. `auth.signedIn`, `auth.email`.
34
34
 
35
- ## @odla-ai/calendar — read-only Google booking mirror
35
+ ## @odla-ai/calendar — live Google Calendar booking
36
36
 
37
37
  Calendar requires `services: ["db", "calendar"]` plus a `calendar.google`
38
- block. Normal provision opens a second, state-bound Google checkpoint issued by the platform for the
39
- human to grant `calendar.events.readonly`; Google tokens stay in the hosted
40
- connector and never become app/CLI secrets.
38
+ block (`availabilityCalendars` per env, optional `bookingCalendar`). Normal
39
+ provision opens a second, state-bound Google checkpoint issued by the
40
+ platform; the human grants the booking scopes in a browser. Google tokens
41
+ stay platform-side and never become app/CLI secrets. Google Calendar is the
42
+ single source of truth — odla stores no calendar or attendee data, and
43
+ nothing syncs.
41
44
 
42
45
  ```ts
43
46
  // Trusted Worker only — the admin key must never enter browser code.
44
- const calendar = initCalendar({
45
- appId: env.ODLA_TENANT,
47
+ const cal = initCalendar({
48
+ appId: env.ODLA_APP_ID, // registry app id
49
+ env: env.ODLA_ENV,
46
50
  adminToken: env.ODLA_API_KEY,
47
- endpoint: env.ODLA_ENDPOINT,
51
+ endpoint: env.ODLA_PLATFORM ?? "https://odla.ai",
48
52
  });
49
- const next = await calendar.bookings.next("member@example.com");
53
+ const fb = await cal.availability.freeBusy({ timeMin, timeMax });
54
+ const slots = computeBookableSlots(fb.busy, { from: timeMin, to: timeMax,
55
+ timezone, slotMinutes: 30 });
56
+ const { booking } = await cal.actions.create(
57
+ { summary, startAt, endAt, attendees: [email], timezone, meet: true },
58
+ { idempotencyKey: `booking:${recordId}` }, // retry-safe; Google emails the invite
59
+ );
50
60
  ```
51
61
 
52
- Browser code may query `$bookings` with `@odla-ai/db/client` only under an
53
- explicit view rule, and may import pure formatting helpers from
54
- `@odla-ai/calendar/client`. Do not import `initCalendar` or expose
55
- `ODLA_API_KEY` in a browser. Provider actions are not part of the read-only
56
- slice; a `calendar_not_connected`/501 action response is not a reason to widen
57
- credentials or request a Google token.
62
+ Store the returned `eventId`/`meetUrl` on the app's own record;
63
+ `actions.reschedule(eventId, …)` and `actions.cancel(eventId)` use it. A
64
+ taken slot rejects with non-retryable `calendar_slot_unavailable` refetch
65
+ slots, don't retry. Browser code never calls `initCalendar` or the platform
66
+ calendar routes; it hits the app's own endpoints and may import pure helpers
67
+ (`computeBookableSlots`, formatters) from `@odla-ai/calendar/client`.
68
+ `SlotPicker` from `@odla-ai/ui` renders the slot output directly.
58
69
 
59
70
  After consent, use `odla-ai calendar calendars --env dev --json` to discover
60
- selectable ids, edit the checked-in list, and optionally set a public per-env
61
- `bookingPageUrl` for Appointment Schedule embed/link mode. Re-provision, then verify with
62
- `calendar status --json` and `smoke`.
71
+ selectable ids and edit the checked-in list. Re-provision, then verify
72
+ `calendar status --env dev` reports `bookable: yes` and `smoke` passes. A
73
+ pre-pivot read-only grant reports `degraded`/`calendar_reconsent_required`;
74
+ re-run `calendar connect`.
63
75
 
64
76
  ## @odla-ai/ai — inference (Claude / GPT / Gemini)
65
77
 
@@ -71,6 +83,31 @@ await ai.chat({ messages }); // provider/model + key resolved from the platform
71
83
  No API key in your code or env — it lives in the tenant vault; `provision`
72
84
  stores it when the configured `ai.keyEnv` is set at provision time.
73
85
 
86
+ ## @odla-ai/crm — records, pipelines, follow-ups, and contactability
87
+
88
+ Define the CRM once in an app module, mount `createCrmRoutes` in trusted Worker
89
+ code behind the app's admin authorization, and render the optional admin kit
90
+ from `@odla-ai/crm/ui`. Declare `createCrmIntegration(crm, options)` under
91
+ `odla.config.mjs` `integrations`—never under `services`.
92
+
93
+ ```ts
94
+ import { createCrmIntegration } from "@odla-ai/crm";
95
+ import { crm } from "./src/crm.js";
96
+
97
+ export default {
98
+ app: { id: "my-app", name: "My App" },
99
+ services: ["db"],
100
+ integrations: [createCrmIntegration(crm, { basePath: "/api/crm" })],
101
+ links: { dev: "https://dev.example.com" },
102
+ };
103
+ ```
104
+
105
+ Provision merges the CRM schema/rules and creates `crm_config` only when
106
+ absent. Doctor checks the composed contract offline; smoke calls the records
107
+ route anonymously and requires `401`. The CLI does not edit the Worker or
108
+ invent `authorize`. Billing is an optional injected `BillingProvider`; reuse
109
+ app-owned payment code when present rather than assuming another package.
110
+
74
111
  ## @odla-ai/o11y — observability (Cloudflare Workers)
75
112
 
76
113
  ```ts
@@ -2,7 +2,7 @@
2
2
  name: odla-migrate
3
3
  description: >
4
4
  Migrate a static site (e.g. GitHub Pages) to odla on Cloudflare in safe
5
- phases, then add a database, optional read-only calendar mirror, Clerk login, and AI. Use when the user wants to
5
+ phases, then add a database, optional Google Calendar booking, Clerk login, and AI. Use when the user wants to
6
6
  move a static or GitHub Pages site to Cloudflare/odla, or to add a backend,
7
7
  database, login/auth, or AI features to a static site via odla.
8
8
  runbookOrder:
@@ -49,10 +49,12 @@ installed skill at `../odla/SKILL.md` instead.
49
49
  approved o11y rotation with `--push-secrets` so the Worker is updated in the
50
50
  same run. If the final Wrangler transfer fails, use the CLI's printed
51
51
  non-rotating `secrets push` retry; never rotate again just to retry delivery.
52
- 7. Existing calendar embeds are public links, not proof of a booking. Preserve
53
- them via `bookingPageUrl`; never claim reconciliation until the read-only
54
- mirror is connected and the app correlates `$bookings` deliberately. Google
55
- OAuth tokens never enter the repo, CLI, or chat.
52
+ 7. Calendar booking is server-side only. `initCalendar` and the platform's
53
+ calendar routes authenticate with the app's full `ODLA_API_KEY` — never
54
+ call them from a browser; the browser talks to the app's own Worker
55
+ endpoints. An old public embed link is not a booking flow; preserve it via
56
+ `bookingPageUrl` only until the native flow is verified. Google OAuth
57
+ tokens never enter the repo, CLI, or chat.
56
58
 
57
59
  ## Phase state machine
58
60
 
@@ -108,7 +110,7 @@ Read the current phase's file when you enter it — not before:
108
110
  - references/phase-0-preflight.md
109
111
  - references/phase-1-static.md
110
112
  - references/phase-2-db.md
111
- - references/phase-2b-calendar.md (optional: preserve an embed and add authoritative read-only reconciliation)
113
+ - references/phase-2b-calendar.md (optional: live Google Calendar booking slots, create/reschedule/cancel through the app's own Worker)
112
114
  - references/phase-3-auth.md
113
115
  - references/phase-3b-user-sync.md (optional: mirror Clerk users into $users)
114
116
  - references/phase-4-ai.md
@@ -11,8 +11,8 @@ opens the review page in interactive terminals; loading it alone is inert.
11
11
 
12
12
  ## Steps
13
13
 
14
- 1. Require `npm view @odla-ai/cli@0.10.2 version` to succeed, then run
15
- `npm i -D --save-exact @odla-ai/cli@0.10.2` and `npm i @odla-ai/db`.
14
+ 1. Require `npm view @odla-ai/cli@0.13.0 version` to succeed, then run
15
+ `npm i -D --save-exact @odla-ai/cli@0.13.0` and `npm i @odla-ai/db`.
16
16
  2. `npx @odla-ai/cli init --app-id <id> --name "<Name>" --env dev --services db`
17
17
  Review `odla.config.mjs`. Keep `envs: ["dev"]` — prod is Phase 5. Set
18
18
  `links.dev` to the URL the Phase 1 `wrangler deploy` **actually printed** —
@@ -1,42 +1,130 @@
1
- # Phase 2b — Google Calendar (optional, read-only)
2
-
3
- Use this phase only when the existing site has a Google Appointment Schedule
4
- link/embed or locally reported meeting state. Keep the old booking experience
5
- working while adding an authoritative read-only mirror.
6
-
7
- Human obligations: review attendee-data retention and complete the second,
8
- server-issued Google consent page. They never paste an OAuth code or token.
9
-
10
- 1. Inventory the old behavior. Distinguish a public appointment-page link from
11
- actual event create/reschedule/cancel APIs. Do not promise more than exists.
12
- 2. Require `npm view @odla-ai/calendar@0.1.1 version` to succeed, then install
13
- `npm i --save-exact @odla-ai/calendar@0.1.1` as a runtime dependency. An
14
- exact-version `E404` blocks this phase; do not substitute a git checkout or
15
- another version. Add `"calendar"` beside
16
- `"db"` in services and configure:
17
- - `calendar.google.calendars.dev` (start with `"primary"`);
18
- - optional summary/organizer/attendee match filters;
19
- - deliberate `attendeePolicy: "full" | "hashed"`;
20
- - the old public Appointment Schedule URL as `bookingPageUrl.dev` when
21
- preserving the embed/link. It is public configuration, not a secret.
22
- 3. Run `doctor`, then `provision --dry-run`. Show the human the calendars,
23
- attendee policy, public booking page, and read-only scope.
24
- 4. Run normal dev provision. After odla device approval, pause again while the
25
- human grants Google `calendar.events.readonly`; the CLI follows initial sync.
26
- 5. Run `calendar calendars --env dev --json`, refine the checked-in ids, and
27
- re-provision. Verify `calendar status --env dev --json` and `smoke --env dev`.
28
- 6. Put `initCalendar` only in trusted Worker code. Browser code may keep using
29
- the public `bookingPageUrl`, query `$bookings` through explicit db rules, or
30
- import pure helpers from `@odla-ai/calendar/client`. Never expose
31
- `ODLA_API_KEY`.
32
- 7. Replace self-reported meeting state only after an explicit, tested
33
- application-to-booking correlation exists. A matching attendee email may be
34
- sufficient for some apps; do not silently regress later workflow statuses.
35
-
36
- This slice cannot create, reschedule, or cancel Google events. A static picker
37
- continues to be Google's sealed UI; odla supplies configuration, mirror reads,
38
- and reconciliation.
39
-
40
- Gate: the old embed/link still works, status is healthy, the selected calendars
41
- match config, `$bookings` smoke succeeds, no Google credential exists in local
42
- files, and `MIGRATION.md` records the disclosure/correlation decision.
1
+ # Phase 2b — Google Calendar booking (optional)
2
+
3
+ Use this phase when the site should offer real scheduling: visitors pick a
4
+ bookable slot and the app creates a live Google Calendar event — with a Meet
5
+ link and an invitation email that Google itself sends. Google Calendar is the
6
+ single source of truth; odla stores no calendar or attendee data. There is no
7
+ sync, no mirror, and no `$bookings` namespace.
8
+
9
+ Human obligations: complete the server-issued Google consent page in a
10
+ browser (they never paste an OAuth code or token), and receive the test
11
+ invitation email during verification.
12
+
13
+ Boundary that shapes every step: `initCalendar` and the platform's calendar
14
+ routes are **server-side only** they authenticate with the app's full
15
+ `ODLA_API_KEY`. Never call them from a browser; the browser talks only to
16
+ this app's own Worker endpoints and may import pure helpers from
17
+ `@odla-ai/calendar/client`.
18
+
19
+ 1. Require `npm view @odla-ai/calendar@0.2.0 version` to succeed, then
20
+ install `npm i --save-exact @odla-ai/calendar@0.2.0` as a runtime
21
+ dependency. An exact-version `E404` blocks this phase; do not substitute a
22
+ git checkout or another version.
23
+ 2. Add `"calendar"` beside `"db"` in services and author the config block —
24
+ booking rides the existing db key, so calendar adds no new secret:
25
+
26
+ ```js
27
+ calendar: {
28
+ google: {
29
+ availabilityCalendars: { dev: ["primary"] }, // 1–10 per env
30
+ bookingCalendar: { dev: "primary" }, // optional; defaults to first availability calendar
31
+ bookingPageUrl: { dev: null }, // optional legacy fallback link
32
+ },
33
+ },
34
+ ```
35
+
36
+ 3. Run `doctor`, then `provision --dry-run`; show the human the calendars and
37
+ the booking scopes. Run normal dev provision. Consent runs last: the CLI
38
+ prints/opens a state-bound Google URL and the human grants access in a
39
+ browser. If the platform connector reports a readiness 503
40
+ (`calendar_*_not_configured`), provision still completes and prints a
41
+ `calendar connect --env dev` resume hint that gap is platform-operator
42
+ work, not yours. Then `calendar calendars --env dev --json` to refine
43
+ checked-in ids, and verify `calendar status --env dev` shows
44
+ `bookable: yes` (a pre-pivot read-only grant shows `degraded` /
45
+ `calendar_reconsent_required`; re-run `calendar connect`).
46
+ 4. Build the **slots endpoint** in the app's Worker — capability-gated like
47
+ every other route (a booking endpoint that ignores Phase 3 auth decisions
48
+ is a regression):
49
+
50
+ ```ts
51
+ import { initCalendar, computeBookableSlots } from "@odla-ai/calendar";
52
+
53
+ // GET /api/booking/slots
54
+ const cal = initCalendar({
55
+ appId: env.ODLA_APP_ID,
56
+ env: env.ODLA_ENV,
57
+ adminToken: env.ODLA_API_KEY,
58
+ endpoint: env.ODLA_PLATFORM ?? "https://odla.ai",
59
+ });
60
+ const fb = await cal.availability.freeBusy({
61
+ timeMin: Date.now(),
62
+ timeMax: Date.now() + 14 * 86_400_000,
63
+ });
64
+ const slots = computeBookableSlots(fb.busy, {
65
+ from: fb.timeMin,
66
+ to: fb.timeMax,
67
+ timezone: BOOKING_TIMEZONE, // app-owned config, not user input
68
+ slotMinutes: 30,
69
+ businessHours: { days: [1, 2, 3, 4, 5], startHour: 9, endHour: 17 },
70
+ minNoticeMs: 24 * 3_600_000,
71
+ });
72
+ return Response.json({ slots, timezone: BOOKING_TIMEZONE });
73
+ ```
74
+
75
+ Business hours, slot length, notice, and buffers are this app's product
76
+ decisions — keep them in app config, and reject slot requests wider than
77
+ the window you offer.
78
+ 5. Build the **create endpoint** (`POST /api/booking`). Validate that the
79
+ requested slot is one this app offers (recompute from the same config),
80
+ then create — the platform re-validates availability under its booking
81
+ lease, so a stale slot cannot double-book:
82
+
83
+ ```ts
84
+ const { booking, duplicate } = await cal.actions.create(
85
+ {
86
+ summary: `Intro call — ${name}`,
87
+ startAt, endAt,
88
+ attendees: [email],
89
+ timezone: BOOKING_TIMEZONE,
90
+ meet: true, // Meet link on the event and in the invite
91
+ // notify defaults true → Google emails the invitation
92
+ },
93
+ { idempotencyKey: `booking:${applicationId}` }, // your own stable record id
94
+ );
95
+ ```
96
+
97
+ Store the returned `booking.eventId` and `booking.meetUrl` on the app's
98
+ own record (its odla-db row) — that record is the app's only persistence;
99
+ attendee data lives in Google. On `calendar_slot_unavailable` (409,
100
+ non-retryable) return a "slot taken" response; the client refetches
101
+ `/api/booking/slots` and re-picks. On retryable
102
+ `calendar_booking_in_progress`, retry once after a short delay.
103
+ 6. Reschedule/cancel go through the stored id:
104
+ `cal.actions.reschedule(eventId, { startAt, endAt })` and
105
+ `cal.actions.cancel(eventId)` (idempotent) — expose them as app endpoints
106
+ gated to the record's owner. Attendees can also act through Google's own
107
+ invitation flows; treat Google as authoritative.
108
+ 7. Frontend. Two supported shapes:
109
+ - **`@odla-ai/ui` SlotPicker** — renders the slots response directly
110
+ (`{ slots, timezone, onSelect }`). On a non-framework static site use a
111
+ Preact island: a small entry that mounts `<SlotPicker>` into a div,
112
+ bundled with esbuild as an IIFE with `react`/`react-dom` aliased to
113
+ `preact/compat`, committed as a static asset next to the site's other
114
+ assets.
115
+ - **Plain fetch + buttons** — `fetch("/api/booking/slots")`, render times
116
+ with `formatBookingRange` from `@odla-ai/calendar/client`, POST the
117
+ chosen slot.
118
+ Either way the browser only ever talks to this app's Worker.
119
+ 8. If the old site had a Google Appointment Schedule embed/link, keep it
120
+ working via `bookingPageUrl` until the native flow is verified, then
121
+ retire it deliberately.
122
+
123
+ Gate (live dev round-trip, all of it): a booking made through the site's own
124
+ UI creates the event on the booking calendar with a Meet link; the Google
125
+ invitation email arrives in the test attendee's inbox; a repeat POST with the
126
+ same idempotency key returns `duplicate: true` (no second event); a taken
127
+ slot returns 409 and the UI refetches slots; reschedule and cancel work via
128
+ the stored `eventId`; zero attendee data is persisted anywhere except the
129
+ app's own tenant record; no Google credential exists in local files; and
130
+ `MIGRATION.md` records the booking-flow decision.
@@ -15,6 +15,9 @@
15
15
  | `o11y_…` (o11y ingest token) | wrangler secret `ODLA_O11Y_TOKEN` + `.odla/credentials.local.json` (0600) + `.dev.vars` | only if the app enables o11y; provision issues/reuses it and moves it alongside the db key; never a var, never chat |
16
16
  | `ODLA_ENDPOINT` / `ODLA_TENANT` / `ODLA_PLATFORM` / `ODLA_APP_ID` / `ODLA_ENV` | wrangler `vars` | not secrets; keep them set in every env block |
17
17
  | `ODLA_O11Y_ENDPOINT` / `ODLA_O11Y_SERVICE` | `.dev.vars` from provision or optional wrangler `vars` overrides | not secrets; public ingest defaults to `https://o11y.odla.ai` and `ODLA_APP_ID` when the token is present |
18
+ | `GOOGLE_CALENDAR_CLIENT_ID` / `GOOGLE_CALENDAR_CLIENT_SECRET` / `CALENDAR_TOKEN_MASTER_KEY` | platform-operator Worker secrets on `odla-apps` | never part of a customer migration; never in chat, the repo, or the app's wrangler config — a readiness 503 (`calendar_*_not_configured`) means the operator sets these, not you |
19
+ | Google calendar grant (OAuth tokens) | platform-side, sealed after human browser consent | the human clicks the server-issued consent URL; no code or token is ever pasted, and the CLI/app/repo never hold one |
20
+ | calendar booking auth (app side) | none — rides the existing `ODLA_API_KEY` | enabling calendar adds NO new app secret; `initCalendar` uses the app's server db key, server-side only, never in a browser |
18
21
 
19
22
  System AI provider credentials are platform-owned and never enter a customer
20
23
  migration. `odla-ai security run` receives only app/run-bound role grants; do
@@ -29,11 +29,22 @@ prints what it verified against.
29
29
 
30
30
  Cause: the `odla_dev_…` token expired (~24h) or the handshake was never
31
31
  approved.
32
- Fix: re-run `npx @odla-ai/cli provision` — it starts a fresh handshake; the
33
- matching existing account must be supplied with `--email` or
34
- `ODLA_USER_EMAIL`, then signs in, reviews, and approves the exact code at the
35
- printed URL. Never ask for a password or session token. Use `--no-open` in
36
- non-interactive shells if the browser launch misbehaves.
32
+ Fix: re-run `npx @odla-ai/cli provision` — it resumes a still-pending
33
+ handshake if one exists, else starts a fresh one; the matching existing
34
+ account must be supplied with `--email` or `ODLA_USER_EMAIL`, then signs in,
35
+ reviews, and approves the exact code at the printed URL. Never ask for a
36
+ password or session token.
37
+
38
+ ## Handshake exits with code 75 / "handshake still pending"
39
+
40
+ Cause: nobody approved within the wait cap (90s outside an interactive
41
+ terminal; `--wait <seconds>` overrides). This is normal in agent shells and
42
+ loses nothing — the handshake is persisted in `.odla/handshake.local.json`.
43
+ Fix: relay the printed approval URL to the human **verbatim** (browser
44
+ launch is best-effort — a sandboxed shell can report success with no tab
45
+ appearing), wait for them to approve, then re-run the same command; it
46
+ resumes the same code and collects the token. Do not loop unattended
47
+ retries; each handshake lives ~10 minutes.
37
48
 
38
49
  ## `smoke` fails: missing credentials
39
50