@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/README.md +49 -8
- package/REQUIREMENTS.md +11 -3
- package/dist/bin.cjs +874 -382
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -2
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-DC4MIB4X.js → chunk-I7XDJZB6.js} +791 -301
- package/dist/chunk-I7XDJZB6.js.map +1 -0
- package/dist/index.cjs +877 -385
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -62
- package/dist/index.d.ts +114 -62
- package/dist/index.js +5 -5
- package/package.json +1 -1
- package/skills/odla/SKILL.md +23 -13
- package/skills/odla/references/build.md +26 -16
- package/skills/odla/references/sdks.md +54 -17
- package/skills/odla-migrate/SKILL.md +8 -6
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/skills/odla-migrate/references/phase-2b-calendar.md +130 -42
- package/skills/odla-migrate/references/secrets-map.md +3 -0
- package/skills/odla-migrate/references/troubleshooting.md +16 -5
- package/dist/chunk-DC4MIB4X.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import {
|
|
3
3
|
AGENT_HARNESSES,
|
|
4
4
|
CAPABILITIES,
|
|
5
|
-
|
|
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-
|
|
41
|
+
} from "./chunk-I7XDJZB6.js";
|
|
42
42
|
export {
|
|
43
43
|
AGENT_HARNESSES,
|
|
44
44
|
CAPABILITIES,
|
|
45
|
-
|
|
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.
|
|
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",
|
package/skills/odla/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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;
|
|
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.
|
|
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),
|
|
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
|
|
102
|
-
and the
|
|
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
|
|
124
|
-
calendars --json` to discover ids
|
|
125
|
-
|
|
126
|
-
`--yes` and
|
|
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,
|
|
130
|
-
transfer. You own source changes — install
|
|
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
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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 --
|
|
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
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
`@odla-ai/calendar/client`
|
|
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 —
|
|
35
|
+
## @odla-ai/calendar — live Google Calendar booking
|
|
36
36
|
|
|
37
37
|
Calendar requires `services: ["db", "calendar"]` plus a `calendar.google`
|
|
38
|
-
block
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
45
|
-
appId: env.
|
|
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.
|
|
51
|
+
endpoint: env.ODLA_PLATFORM ?? "https://odla.ai",
|
|
48
52
|
});
|
|
49
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
`
|
|
62
|
-
|
|
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
|
|
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.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
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.
|
|
15
|
-
`npm i -D --save-exact @odla-ai/cli@0.
|
|
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
|
|
2
|
-
|
|
3
|
-
Use this phase
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
and
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
`ODLA_USER_EMAIL`, then signs in,
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|