@nbt-dev/nbt 0.0.9 → 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/dist/nbt.js +117 -36
- package/package.json +1 -1
- package/stdlib/auth/migrations/20260614000000_add_user_devtools_settings/migration.nbt +4 -0
- package/stdlib/auth/migrations/20260614000000_add_user_devtools_settings/schema_snapshot.nbt +59 -0
- package/stdlib/auth/schema.nbt +5 -30
- package/stdlib/calendar/schema.nbt +2 -3
- package/stdlib/crm/schema.nbt +3 -4
- package/stdlib/design/schema.nbt +2 -2
- package/stdlib/dns/schema.nbt +3 -3
- package/stdlib/email/schema.nbt +12 -11
- package/stdlib/ingest/schema.nbt +4 -4
- package/stdlib/notifications/schema.nbt +2 -2
- package/stdlib/phone/schema.nbt +9 -10
- package/stdlib/workflows/schema.nbt +69 -21
- package/vendor/linux-x64/cartridges/auth/migrations/20260614000000_add_user_devtools_settings/migration.nbt +4 -0
- package/vendor/linux-x64/cartridges/auth/migrations/20260614000000_add_user_devtools_settings/schema_snapshot.nbt +59 -0
- package/vendor/linux-x64/cartridges/auth/schema.nbt +5 -30
- package/vendor/linux-x64/cartridges/calendar/schema.nbt +2 -3
- package/vendor/linux-x64/cartridges/crm/schema.nbt +3 -4
- package/vendor/linux-x64/cartridges/design/schema.nbt +2 -2
- package/vendor/linux-x64/cartridges/dns/schema.nbt +3 -3
- package/vendor/linux-x64/cartridges/email/schema.nbt +12 -11
- package/vendor/linux-x64/cartridges/ingest/schema.nbt +4 -4
- package/vendor/linux-x64/cartridges/notifications/schema.nbt +2 -2
- package/vendor/linux-x64/cartridges/phone/schema.nbt +9 -10
- package/vendor/linux-x64/cartridges/workflows/schema.nbt +69 -21
- package/vendor/linux-x64/console +0 -0
- package/vendor/linux-x64/nbt +0 -0
- package/stdlib/crm/adapters/gohighlevel/README.md +0 -85
- package/stdlib/crm/adapters/gohighlevel/tests/README.md +0 -159
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_138fields.json +0 -222
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_140fields.json +0 -219
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_alt.json +0 -212
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_changed.json +0 -102
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_created.json +0 -95
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_full.json +0 -213
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_sparse.json +0 -161
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_update_a.json +0 -197
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_update_b.json +0 -197
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/opportunity_changed.json +0 -85
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/opportunity_created.json +0 -85
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_pilot.json +0 -43
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_with_price_closed.json +0 -7
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_with_price_open.json +0 -7
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_appointment_delete.json +0 -1
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_calendar_update.json +0 -1
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_contact_create.json +0 -1
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_opp_status_update.json +0 -1
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_opportunity_pilot.json +0 -16
- package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_pipelines_pilot.json +0 -137
|
@@ -1,44 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
entity Worker {
|
|
1
|
+
# An immutable, versioned workflow bundle (the JS artifact). Deploying new code
|
|
2
|
+
# registers a new version; in-flight executions keep replaying against the version
|
|
3
|
+
# they started on (the journal-header pin), so a code change can never corrupt a
|
|
4
|
+
# running instance. A version's source is retained while ≥1 execution pins it and
|
|
5
|
+
# dropped once the last drains (refcount drain-GC, in-memory). Durable so a pinned
|
|
6
|
+
# version survives restart.
|
|
7
|
+
entity WorkflowBundle {
|
|
11
8
|
id: ulid
|
|
12
9
|
createdAt: DateTime @default(now())
|
|
13
10
|
updatedAt: DateTime @updatedAt
|
|
14
|
-
|
|
11
|
+
version: u32
|
|
12
|
+
source: string
|
|
15
13
|
contentHash: string
|
|
16
|
-
queue?: string
|
|
17
|
-
# TODO: We need some sort of termination options like a restart policy
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
#
|
|
16
|
+
# A single durable workflow instance. The journal is the WorkflowExecutionEvent
|
|
17
|
+
# rows owned by this row (ordered by seq); status + result/error are the
|
|
18
|
+
# materialized terminal state. cursor = number of resolved host commands
|
|
19
|
+
# (== count of HOST_CALL_RESULT events) — the replay ordinal high-water mark.
|
|
21
20
|
entity WorkflowExecution {
|
|
22
21
|
id: ulid
|
|
23
22
|
createdAt: DateTime @default(now())
|
|
24
23
|
updatedAt: DateTime @updatedAt
|
|
25
|
-
status: string
|
|
24
|
+
status: string # RUNNING (incl. async host call in flight) | SUSPENDED | COMPLETED | FAILED | CANCELLED
|
|
25
|
+
workflowName: string
|
|
26
|
+
args?: string # JSON arguments passed to runWorkflow
|
|
27
|
+
result?: string # JSON return value (COMPLETED)
|
|
28
|
+
error?: string # message + stack (FAILED)
|
|
29
|
+
cursor: u32
|
|
30
|
+
lane?: string # fairness lane; per-lane concurrency is capped so a burst of one lane can't starve others (default "default")
|
|
31
|
+
deployVersion: u32 # the bundle version this instance pins for its entire life (journal-header pin); replay always runs against this exact version
|
|
26
32
|
events: WorkflowExecutionEvent[]
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
#
|
|
35
|
+
# A recurring trigger: fire `workflowName` whenever the cron expression matches.
|
|
36
|
+
# Idempotent per minute-bucket via a deterministic execution id (schedule + bucket),
|
|
37
|
+
# so a tick repeating within the same minute — or a restart mid-minute — never
|
|
38
|
+
# double-fires. Evaluated leader-only by the executor tick.
|
|
39
|
+
entity WorkflowSchedule {
|
|
40
|
+
id: ulid
|
|
41
|
+
createdAt: DateTime @default(now())
|
|
42
|
+
updatedAt: DateTime @updatedAt
|
|
43
|
+
workflowName: string
|
|
44
|
+
cron: string # 5-field: minute hour day-of-month month day-of-week
|
|
45
|
+
args?: string # JSON arguments passed to each fired execution
|
|
46
|
+
lane?: string # lane the fired executions run on
|
|
47
|
+
enabled: bool
|
|
48
|
+
lastFiredBucket: u32 # highest minute-bucket already fired; the durable idempotency watermark (no double-fire across ticks/restart)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# A durable record that a named event fired. Emissions ride the WAL (replicated
|
|
52
|
+
# like any row); the leader-only executor reconcile drains the unprocessed ones,
|
|
53
|
+
# looks up the event's triggers in the storage event registry, and fires each
|
|
54
|
+
# triggered workflow exactly once cluster-wide (only the leader drains; the fired
|
|
55
|
+
# id is derived from emission+index so a crash mid-drain re-fires idempotently).
|
|
56
|
+
entity WorkflowEvent {
|
|
57
|
+
id: ulid
|
|
58
|
+
createdAt: DateTime @default(now())
|
|
59
|
+
updatedAt: DateTime @updatedAt
|
|
60
|
+
event: string # "<slug>:<event>" — the event-registry key
|
|
61
|
+
payload?: string # JSON passed as args to each fired workflow
|
|
62
|
+
processed: bool # set true once the leader has fired this emission's triggers
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
enum WorkflowExecutionEventKind {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
HOST_CALL_INTENT # recorded BEFORE a side effect is performed (durability)
|
|
67
|
+
HOST_CALL_RESULT # the resolved value of a host command (replay source)
|
|
68
|
+
SUSPENDED # parked awaiting an external command (e.g. a signal)
|
|
69
|
+
RESUMED # an external command arrived and re-drove the instance
|
|
70
|
+
COMPLETED # workflow function returned
|
|
71
|
+
FAILED # workflow function threw
|
|
72
|
+
CANCELLED # terminated by an explicit cancel request (cooperative)
|
|
73
|
+
RETRY # an async host call failed transiently and was retried (counter; payload = attempt)
|
|
74
|
+
CONTINUED # ended via continue-as-new; payload = the child execution id carrying the work forward
|
|
36
75
|
}
|
|
37
76
|
|
|
77
|
+
# One journal entry. For HOST_CALL_* events seq is the command ordinal and
|
|
78
|
+
# (capability, target, op) is the descriptor; payload carries the args (INTENT)
|
|
79
|
+
# or the result JSON (RESULT). Lifecycle events (SUSPENDED/RESUMED/COMPLETED/
|
|
80
|
+
# FAILED/CANCELLED/RETRY) carry payload only.
|
|
38
81
|
entity WorkflowExecutionEvent {
|
|
39
82
|
id: ulid
|
|
40
83
|
createdAt: DateTime @default(now())
|
|
41
84
|
updatedAt: DateTime @updatedAt
|
|
42
85
|
execution: WorkflowExecution @relation(onDelete: Cascade) # events are owned by their execution
|
|
86
|
+
seq: u32
|
|
43
87
|
kind: WorkflowExecutionEventKind
|
|
88
|
+
capability?: string
|
|
89
|
+
target?: string
|
|
90
|
+
op?: string
|
|
91
|
+
payload?: string
|
|
44
92
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
entity User {
|
|
2
|
+
name: string
|
|
3
|
+
username?: string
|
|
4
|
+
email?: string
|
|
5
|
+
emailVerified: bool
|
|
6
|
+
externalId?: string
|
|
7
|
+
capsVersion: u32
|
|
8
|
+
devtoolsSettings?: string
|
|
9
|
+
@@index([email])
|
|
10
|
+
@@index([externalId])
|
|
11
|
+
@@unique([email])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
entity UserRole {
|
|
15
|
+
userId: string
|
|
16
|
+
cart: string
|
|
17
|
+
role: string
|
|
18
|
+
@@index([userId])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
entity Session {
|
|
22
|
+
userId: string
|
|
23
|
+
token: string
|
|
24
|
+
expiresAt: DateTime
|
|
25
|
+
ipAddress?: string
|
|
26
|
+
userAgent?: string
|
|
27
|
+
@@index([token])
|
|
28
|
+
@@index([userId])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
entity Account {
|
|
32
|
+
userId: string
|
|
33
|
+
providerId: string
|
|
34
|
+
password: string
|
|
35
|
+
accessToken: string
|
|
36
|
+
refreshToken: string
|
|
37
|
+
idToken: string
|
|
38
|
+
accessTokenExpiresAt: DateTime
|
|
39
|
+
refreshTokenExpiresAt: DateTime
|
|
40
|
+
scope: string
|
|
41
|
+
@@index([userId])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
entity Verification {
|
|
45
|
+
identifier: string
|
|
46
|
+
value: string
|
|
47
|
+
expiresAt: DateTime
|
|
48
|
+
@@index([identifier])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
entity ApiKey {
|
|
52
|
+
name: string
|
|
53
|
+
projectId: string
|
|
54
|
+
start: string
|
|
55
|
+
prefix: string
|
|
56
|
+
key: string
|
|
57
|
+
permissions: string
|
|
58
|
+
roles: string
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "crypto"
|
|
2
2
|
|
|
3
|
-
# Auth policy for the hand-written routes in
|
|
3
|
+
# Auth policy for the hand-written routes in modules/cart/auth.jai (formerly the
|
|
4
4
|
# @public actions + the `authenticate` middleware block). These keep the
|
|
5
5
|
# daemon's public-route + middleware manifest correct: public routes bypass the
|
|
6
6
|
# /api/* auth gate; @middleware tells the proxy this cart exports the global
|
|
@@ -25,7 +25,8 @@ export entity User {
|
|
|
25
25
|
email?: string
|
|
26
26
|
emailVerified: bool
|
|
27
27
|
externalId?: string
|
|
28
|
-
capsVersion: u32
|
|
28
|
+
capsVersion: u32 # capability version; bumped on every role change so cached authorizations invalidate
|
|
29
|
+
devtoolsSettings?: string # per-operator devtools UI preferences, stored as a JSON blob
|
|
29
30
|
|
|
30
31
|
@@index([email])
|
|
31
32
|
@@index([externalId])
|
|
@@ -101,8 +102,8 @@ entity ApiKey {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
# Console-operator SSH public keys. The console daemon authenticates SSH
|
|
104
|
-
# connections by
|
|
105
|
-
#
|
|
105
|
+
# connections by reading these rows directly from storage (in-process —
|
|
106
|
+
# auth cart liveness is not required).
|
|
106
107
|
entity SshKey {
|
|
107
108
|
id: ulid
|
|
108
109
|
createdAt: DateTime @default(now())
|
|
@@ -114,29 +115,3 @@ entity SshKey {
|
|
|
114
115
|
@@index([userId])
|
|
115
116
|
@@unique([blob])
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
jai {
|
|
119
|
-
find_synced_user :: (requestedId: string, email: string) -> (User, bool) {
|
|
120
|
-
if requestedId.count > 0 {
|
|
121
|
-
by_id, by_id_ok := get_user(requestedId);
|
|
122
|
-
if by_id_ok return by_id, true;
|
|
123
|
-
|
|
124
|
-
by_external := find_users_by_externalId(requestedId);
|
|
125
|
-
if by_external.count > 0 {
|
|
126
|
-
u, ok := get_user(by_external[0].id);
|
|
127
|
-
if ok return u, true;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if email.count > 0 {
|
|
132
|
-
by_email := find_users_by_email(email);
|
|
133
|
-
if by_email.count > 0 {
|
|
134
|
-
u, ok := get_user(by_email[0].id);
|
|
135
|
-
if ok return u, true;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
empty: User;
|
|
140
|
-
return empty, false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
@@ -45,9 +45,8 @@ export entity Appointment {
|
|
|
45
45
|
|
|
46
46
|
# Rendered embedding-template text. Persisted alongside the Appointment so
|
|
47
47
|
# similarity search can score (target_text, candidate_text) pairs without
|
|
48
|
-
# re-rendering
|
|
49
|
-
#
|
|
50
|
-
# (D09.10c). Generic surface — every customer composing similarity search
|
|
48
|
+
# re-rendering on every query. Populated at ingest by the customer's
|
|
49
|
+
# pipeline. Generic surface — every customer composing similarity search
|
|
51
50
|
# over Appointments needs it; the *content* of the text is customer policy.
|
|
52
51
|
embedText?: string
|
|
53
52
|
|
|
@@ -77,11 +77,10 @@ export entity Deal {
|
|
|
77
77
|
customData: document
|
|
78
78
|
|
|
79
79
|
# Append-only suffix — fields below this comment must stay at the end
|
|
80
|
-
# of the entity declaration.
|
|
80
|
+
# of the entity declaration. The row codec serializes in declaration order;
|
|
81
81
|
# newly-added fields go *after* existing fields so the on-disk byte
|
|
82
82
|
# layout stays forward-compatible (old records deserialize cleanly with
|
|
83
|
-
# trailing fields default-initialised).
|
|
84
|
-
# codegen in modules/nbt/codegen_serial.jai.
|
|
83
|
+
# trailing fields default-initialised).
|
|
85
84
|
|
|
86
85
|
# D09.02 — generic lifecycle status, customer-domain. mylocalpro reacts
|
|
87
86
|
# to `deal.status == "won"` to mark the source Appointment positive.
|
|
@@ -101,7 +100,7 @@ export entity Deal {
|
|
|
101
100
|
|
|
102
101
|
# Date the deal closed (won or lost). Resolved at GHL adapter ingest
|
|
103
102
|
# from Contact custom field "Date Sold" via CONTACT_LATEST_OPEN_DEAL.
|
|
104
|
-
# Append-only field —
|
|
103
|
+
# Append-only field — the row codec serializes in declaration order, so
|
|
105
104
|
# existing rows deserialize cleanly with this trailing default.
|
|
106
105
|
closedDate?: DateTime
|
|
107
106
|
}
|
|
@@ -136,5 +136,5 @@ entity Page {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
# Behavior (former Design.create/save_version/get_head/list_versions actions +
|
|
139
|
-
# tasks/parse.nbt @task) moved to
|
|
140
|
-
#
|
|
139
|
+
# tasks/parse.nbt @task) moved to hand-written Jai over the generated ORM,
|
|
140
|
+
# which also registers the HTTP routes.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
# - Future: TLS cart, custom gateway-domains cart.
|
|
11
11
|
#
|
|
12
12
|
# Schema only. All behavior (token verify, CF API calls, DNS record CRUD,
|
|
13
|
-
# TXT verify polling) lives in
|
|
14
|
-
#
|
|
13
|
+
# TXT verify polling) lives in hand-written Jai over the generated ORM, which
|
|
14
|
+
# also registers the HTTP routes.
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@ entity DnsProvider {
|
|
|
21
21
|
updatedAt: DateTime @updatedAt
|
|
22
22
|
type: string # "cloudflare"
|
|
23
23
|
label: string # user-facing name ("my CF account")
|
|
24
|
-
apiToken: string #
|
|
24
|
+
apiToken: string # provider API token (stored as-is)
|
|
25
25
|
accountId: string # CF account_id, needed for add_zone + registrar calls (optional)
|
|
26
26
|
status: string # "ACTIVE" | "INVALID" | "DISCONNECTED"
|
|
27
27
|
lastCheckedAt: u64 # ms epoch — updated on connect + test
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Email cartridge — transactional send + synthetic inboxes.
|
|
2
2
|
#
|
|
3
|
-
# Provider: SendGrid
|
|
4
|
-
#
|
|
3
|
+
# Provider: SendGrid. Nothing in the user-facing surface mentions SendGrid;
|
|
4
|
+
# the cart is branded "Email".
|
|
5
5
|
#
|
|
6
6
|
# Per-console model:
|
|
7
7
|
# - Every console auto-provisions `<console>.nbt.dev` as the default mail
|
|
@@ -13,21 +13,22 @@
|
|
|
13
13
|
# register with SendGrid Inbound Parse + Domain Authentication, poll
|
|
14
14
|
# until everything is green.
|
|
15
15
|
#
|
|
16
|
-
# Ops prerequisites (one-time
|
|
17
|
-
# -
|
|
18
|
-
# -
|
|
16
|
+
# Ops prerequisites (one-time):
|
|
17
|
+
# - Provider API key in console env.
|
|
18
|
+
# - Provider event-webhook public key in console env (base64 SPKI for event
|
|
19
|
+
# webhook signature verification).
|
|
19
20
|
# - `*.nbt.dev MX 10 mx.sendgrid.net` at zone level.
|
|
20
21
|
# - SendGrid Inbound Parse wildcard entry + per-console entries (latter are
|
|
21
22
|
# created programmatically by MailDomain.ensure_default).
|
|
22
23
|
|
|
23
|
-
# Behavior (the former main.nbt actions) lives in
|
|
24
|
-
# generated ORM
|
|
25
|
-
#
|
|
24
|
+
# Behavior (the former main.nbt actions) lives in hand-written Jai over the
|
|
25
|
+
# generated ORM, which also registers the HTTP routes. The import below pulls
|
|
26
|
+
# the crypto module into the cart scope.
|
|
26
27
|
import crypto from "crypto"
|
|
27
28
|
|
|
28
|
-
# Auth policy for the hand-written routes (formerly @public actions).
|
|
29
|
-
#
|
|
30
|
-
#
|
|
29
|
+
# Auth policy for the hand-written routes (formerly @public actions). These
|
|
30
|
+
# keep the daemon's public-route manifest correct so the /api/* auth gate is
|
|
31
|
+
# bypassed for SendGrid webhooks.
|
|
31
32
|
@public_route "/api/email/inbound"
|
|
32
33
|
@public_route "/api/email/events"
|
|
33
34
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Behavior (the former main.nbt actions) lives in
|
|
2
|
-
# generated ORM
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# Behavior (the former main.nbt actions) lives in hand-written Jai over the
|
|
2
|
+
# generated ORM, which also registers the HTTP routes. All routes stay
|
|
3
|
+
# auth-gated by the daemon middleware (no @public_route), matching the
|
|
4
|
+
# pre-migration manifest.
|
|
5
5
|
|
|
6
6
|
entity Endpoint {
|
|
7
7
|
id: ulid
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# subscriptions. The portal owns browser permission prompts and service worker
|
|
3
3
|
# registration; this cartridge owns the resulting state.
|
|
4
4
|
#
|
|
5
|
-
# Behavior (the former main.nbt actions) lives in
|
|
6
|
-
#
|
|
5
|
+
# Behavior (the former main.nbt actions) lives in hand-written Jai over the
|
|
6
|
+
# generated ORM, which also registers the HTTP routes.
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
entity Notification {
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# Phone cartridge — Twilio number management, SMS, click-to-call.
|
|
2
2
|
#
|
|
3
|
-
# Per-tenant model
|
|
4
|
-
# - One master Twilio account owned by the platform (SID + auth token
|
|
5
|
-
#
|
|
3
|
+
# Per-tenant model:
|
|
4
|
+
# - One master Twilio account owned by the platform (master SID + auth token
|
|
5
|
+
# supplied via env).
|
|
6
6
|
# - Each tenant gets a TwilioAccount row with its own subaccount SID and
|
|
7
7
|
# auth token. All per-tenant API calls authenticate as the subaccount.
|
|
8
8
|
# - Webhooks are configured to URLs under the tenant's public base
|
|
9
9
|
# (TwilioAccount.publicUrlBase), which the gateway routes back to this
|
|
10
10
|
# cart. Signature verification uses the subaccount's auth token.
|
|
11
11
|
|
|
12
|
-
# Behavior (the former
|
|
13
|
-
#
|
|
14
|
-
# which registers the HTTP routes.
|
|
12
|
+
# Behavior (the former actions) lives in hand-written Jai over the generated
|
|
13
|
+
# ORM, which also registers the HTTP routes.
|
|
15
14
|
|
|
16
|
-
# Auth policy for the hand-written routes (formerly @public actions).
|
|
17
|
-
#
|
|
18
|
-
#
|
|
15
|
+
# Auth policy for the hand-written routes (formerly @public actions). These
|
|
16
|
+
# keep the daemon's public-route manifest correct so the /api/* auth gate is
|
|
17
|
+
# bypassed for Twilio webhooks.
|
|
19
18
|
@public_route "/api/phone/webhook/sms"
|
|
20
19
|
@public_route "/api/phone/webhook/status"
|
|
21
20
|
|
|
@@ -25,7 +24,7 @@ entity TwilioAccount {
|
|
|
25
24
|
updatedAt: DateTime @updatedAt
|
|
26
25
|
name: string # operator-facing label
|
|
27
26
|
subAccountSid: string # AC... — populated by provision()
|
|
28
|
-
authToken: string # subaccount auth token (
|
|
27
|
+
authToken: string # subaccount auth token (stored as-is)
|
|
29
28
|
publicUrlBase: string # e.g. "https://acme.console.app"
|
|
30
29
|
status: string # PENDING | ACTIVE | SUSPENDED | CLOSED
|
|
31
30
|
retentionDays: s32 # 0 = keep message bodies forever; N = null body after N days
|
|
@@ -1,44 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
entity Worker {
|
|
1
|
+
# An immutable, versioned workflow bundle (the JS artifact). Deploying new code
|
|
2
|
+
# registers a new version; in-flight executions keep replaying against the version
|
|
3
|
+
# they started on (the journal-header pin), so a code change can never corrupt a
|
|
4
|
+
# running instance. A version's source is retained while ≥1 execution pins it and
|
|
5
|
+
# dropped once the last drains (refcount drain-GC, in-memory). Durable so a pinned
|
|
6
|
+
# version survives restart.
|
|
7
|
+
entity WorkflowBundle {
|
|
11
8
|
id: ulid
|
|
12
9
|
createdAt: DateTime @default(now())
|
|
13
10
|
updatedAt: DateTime @updatedAt
|
|
14
|
-
|
|
11
|
+
version: u32
|
|
12
|
+
source: string
|
|
15
13
|
contentHash: string
|
|
16
|
-
queue?: string
|
|
17
|
-
# TODO: We need some sort of termination options like a restart policy
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
#
|
|
16
|
+
# A single durable workflow instance. The journal is the WorkflowExecutionEvent
|
|
17
|
+
# rows owned by this row (ordered by seq); status + result/error are the
|
|
18
|
+
# materialized terminal state. cursor = number of resolved host commands
|
|
19
|
+
# (== count of HOST_CALL_RESULT events) — the replay ordinal high-water mark.
|
|
21
20
|
entity WorkflowExecution {
|
|
22
21
|
id: ulid
|
|
23
22
|
createdAt: DateTime @default(now())
|
|
24
23
|
updatedAt: DateTime @updatedAt
|
|
25
|
-
status: string
|
|
24
|
+
status: string # RUNNING (incl. async host call in flight) | SUSPENDED | COMPLETED | FAILED | CANCELLED
|
|
25
|
+
workflowName: string
|
|
26
|
+
args?: string # JSON arguments passed to runWorkflow
|
|
27
|
+
result?: string # JSON return value (COMPLETED)
|
|
28
|
+
error?: string # message + stack (FAILED)
|
|
29
|
+
cursor: u32
|
|
30
|
+
lane?: string # fairness lane; per-lane concurrency is capped so a burst of one lane can't starve others (default "default")
|
|
31
|
+
deployVersion: u32 # the bundle version this instance pins for its entire life (journal-header pin); replay always runs against this exact version
|
|
26
32
|
events: WorkflowExecutionEvent[]
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
#
|
|
35
|
+
# A recurring trigger: fire `workflowName` whenever the cron expression matches.
|
|
36
|
+
# Idempotent per minute-bucket via a deterministic execution id (schedule + bucket),
|
|
37
|
+
# so a tick repeating within the same minute — or a restart mid-minute — never
|
|
38
|
+
# double-fires. Evaluated leader-only by the executor tick.
|
|
39
|
+
entity WorkflowSchedule {
|
|
40
|
+
id: ulid
|
|
41
|
+
createdAt: DateTime @default(now())
|
|
42
|
+
updatedAt: DateTime @updatedAt
|
|
43
|
+
workflowName: string
|
|
44
|
+
cron: string # 5-field: minute hour day-of-month month day-of-week
|
|
45
|
+
args?: string # JSON arguments passed to each fired execution
|
|
46
|
+
lane?: string # lane the fired executions run on
|
|
47
|
+
enabled: bool
|
|
48
|
+
lastFiredBucket: u32 # highest minute-bucket already fired; the durable idempotency watermark (no double-fire across ticks/restart)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# A durable record that a named event fired. Emissions ride the WAL (replicated
|
|
52
|
+
# like any row); the leader-only executor reconcile drains the unprocessed ones,
|
|
53
|
+
# looks up the event's triggers in the storage event registry, and fires each
|
|
54
|
+
# triggered workflow exactly once cluster-wide (only the leader drains; the fired
|
|
55
|
+
# id is derived from emission+index so a crash mid-drain re-fires idempotently).
|
|
56
|
+
entity WorkflowEvent {
|
|
57
|
+
id: ulid
|
|
58
|
+
createdAt: DateTime @default(now())
|
|
59
|
+
updatedAt: DateTime @updatedAt
|
|
60
|
+
event: string # "<slug>:<event>" — the event-registry key
|
|
61
|
+
payload?: string # JSON passed as args to each fired workflow
|
|
62
|
+
processed: bool # set true once the leader has fired this emission's triggers
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
enum WorkflowExecutionEventKind {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
66
|
+
HOST_CALL_INTENT # recorded BEFORE a side effect is performed (durability)
|
|
67
|
+
HOST_CALL_RESULT # the resolved value of a host command (replay source)
|
|
68
|
+
SUSPENDED # parked awaiting an external command (e.g. a signal)
|
|
69
|
+
RESUMED # an external command arrived and re-drove the instance
|
|
70
|
+
COMPLETED # workflow function returned
|
|
71
|
+
FAILED # workflow function threw
|
|
72
|
+
CANCELLED # terminated by an explicit cancel request (cooperative)
|
|
73
|
+
RETRY # an async host call failed transiently and was retried (counter; payload = attempt)
|
|
74
|
+
CONTINUED # ended via continue-as-new; payload = the child execution id carrying the work forward
|
|
36
75
|
}
|
|
37
76
|
|
|
77
|
+
# One journal entry. For HOST_CALL_* events seq is the command ordinal and
|
|
78
|
+
# (capability, target, op) is the descriptor; payload carries the args (INTENT)
|
|
79
|
+
# or the result JSON (RESULT). Lifecycle events (SUSPENDED/RESUMED/COMPLETED/
|
|
80
|
+
# FAILED/CANCELLED/RETRY) carry payload only.
|
|
38
81
|
entity WorkflowExecutionEvent {
|
|
39
82
|
id: ulid
|
|
40
83
|
createdAt: DateTime @default(now())
|
|
41
84
|
updatedAt: DateTime @updatedAt
|
|
42
85
|
execution: WorkflowExecution @relation(onDelete: Cascade) # events are owned by their execution
|
|
86
|
+
seq: u32
|
|
43
87
|
kind: WorkflowExecutionEventKind
|
|
88
|
+
capability?: string
|
|
89
|
+
target?: string
|
|
90
|
+
op?: string
|
|
91
|
+
payload?: string
|
|
44
92
|
}
|
package/vendor/linux-x64/console
CHANGED
|
Binary file
|
package/vendor/linux-x64/nbt
CHANGED
|
Binary file
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# GHL Adapter
|
|
2
|
-
|
|
3
|
-
Webhook ingest → CRM persistence as composable tasks. The adapter classifies
|
|
4
|
-
inbound GHL Workflow webhook payloads and upserts the corresponding Contact
|
|
5
|
-
or Deal directly. Appointment persistence lives in the calendar cart's
|
|
6
|
-
parallel adapter (`cartridges/core/calendar/adapters/gohighlevel/`) because
|
|
7
|
-
Calendar/Appointment are calendar-cart entities.
|
|
8
|
-
|
|
9
|
-
No transformation, normalization, geocoding, AI scoring, or write-back to
|
|
10
|
-
GHL. Those are downstream client-cart concerns. Read-side tasks (backfill,
|
|
11
|
-
sync recovery) come back when there's a real consumer.
|
|
12
|
-
|
|
13
|
-
## Tasks (queue names = `crm.<task>`)
|
|
14
|
-
|
|
15
|
-
| Task | Inputs | Outputs |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| `crm.ghl_classify_event_type` | `body_json` | `event_type`, `tenant_org`, `tenant_project` |
|
|
18
|
-
| `crm.ghl_persist_contact` | `body_json` | `contact_id`, `created`, `ok`, `error` |
|
|
19
|
-
| `crm.ghl_persist_opportunity` | `body_json` | `deal_id`, `created`, `ok`, `error` |
|
|
20
|
-
|
|
21
|
-
Plus, in the calendar cart:
|
|
22
|
-
|
|
23
|
-
| Task | Inputs | Outputs |
|
|
24
|
-
|---|---|---|
|
|
25
|
-
| `calendar.ghl_persist_appointment` | `body_json` | `appointment_id`, `calendar_id`, `ok`, `error` |
|
|
26
|
-
|
|
27
|
-
`event_type` outputs from classify: `ContactCreate`, `ContactChange`,
|
|
28
|
-
`OpportunityCreate`, `OpportunityChange`, `AppointmentCreate`,
|
|
29
|
-
`AppointmentChange`, `Unknown`. The 7th GHL workflow "Pipeline Stage Changed
|
|
30
|
-
Sync" aliases onto `OpportunityChange` because the payload shape is
|
|
31
|
-
identical.
|
|
32
|
-
|
|
33
|
-
## Webhook entry
|
|
34
|
-
|
|
35
|
-
GHL POSTs to ingest:
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
POST /api/ingest/endpoint/receive?e=mylocalpro-ghl
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
The `Endpoint` row for slug `mylocalpro-ghl` carries `systemId` pointing at
|
|
42
|
-
the per-client System graph. After Payload persistence, ingest fires
|
|
43
|
-
`queue_exec_start("system.<systemId>", { payloadId, endpointSlug, endpointId, body_json })`.
|
|
44
|
-
|
|
45
|
-
The first task in the per-client graph is `crm.ghl_classify_event_type`,
|
|
46
|
-
followed by a `branch` on `event_type` to one of the persist tasks
|
|
47
|
-
(`ghl_persist_contact`, `ghl_persist_opportunity`,
|
|
48
|
-
`calendar.ghl_persist_appointment`).
|
|
49
|
-
|
|
50
|
-
## Files
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
main.nbt # composes the persist tasks
|
|
54
|
-
native/persist.jai # ghl_apply_contact_payload, ghl_apply_opportunity_payload
|
|
55
|
-
tasks/classify_event_type.nbt
|
|
56
|
-
tasks/persist_contact.nbt
|
|
57
|
-
tasks/persist_opportunity.nbt
|
|
58
|
-
tests/fixtures/webhooks/ # 4 real-shape fixtures (contact_*/opportunity_*)
|
|
59
|
-
tests/README.md # fixture provenance + classifier expectations
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Native imports are declared at the cart root (`crm/cartridge.nbt`) because
|
|
63
|
-
the codegen's native-file copy step assumes paths are cart-root-relative.
|
|
64
|
-
|
|
65
|
-
The calendar cart mirrors this layout for appointment persistence:
|
|
66
|
-
`cartridges/core/calendar/adapters/gohighlevel/`.
|
|
67
|
-
|
|
68
|
-
## External-PK = local-PK
|
|
69
|
-
|
|
70
|
-
GHL ids are written into entity `id` directly: a Contact for
|
|
71
|
-
`contact_id="ctc_xyz"` lives at `Contact.id="ctc_xyz"`, an Opportunity for
|
|
72
|
-
`id="opp_001"` lives at `Deal.id="opp_001"`, an Appointment for
|
|
73
|
-
`calendar.appointmentId="apt_xyz"` lives at `Appointment.id="apt_xyz"`. No
|
|
74
|
-
`externalId` indirection — the next webhook update keys the same row.
|
|
75
|
-
|
|
76
|
-
## Adding a persist branch
|
|
77
|
-
|
|
78
|
-
1. Add the helper to `native/persist.jai` (or a calendar/agent/etc. cart's
|
|
79
|
-
`native/persist.jai` if the target entity lives there).
|
|
80
|
-
2. Create `tasks/persist_<name>.nbt` with `input body_json: string` and the
|
|
81
|
-
handler delegating to the helper.
|
|
82
|
-
3. Import the task in `main.nbt`.
|
|
83
|
-
4. Update `tasks/classify_event_type.nbt` if a new `event_type` is needed.
|
|
84
|
-
5. `jai first.jai` — task appears in the cart's `contract.json` and at
|
|
85
|
-
`/_console/task-catalog`.
|