spine-framework 0.3.9 → 0.3.11

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.
@@ -185,42 +185,22 @@ export function shouldOfferClaim(): ClaimPrompt | null {
185
185
 
186
186
  // ─── SIGNAL SENDING ───────────────────────────────────────────────────────────
187
187
 
188
- export interface FrameworkInstallSignal {
189
- signal_type: 'framework_install'
190
- install_serial: string
191
- package: string
192
- version: string
193
- anonymous_id: string
194
- machine_fingerprint: string
195
- platform: string
196
- timestamp: string
197
- }
198
-
199
- export interface AppInstallSignal {
200
- signal_type: 'app_install'
201
- package: string
202
- version: string
203
- framework_install_serial: string
204
- app_serial: string
205
- timestamp: string
206
- }
207
-
208
188
  export async function sendFrameworkInstallSignal(): Promise<void> {
209
189
  const serialData = ensureSerialData()
210
190
 
211
- const signal: FrameworkInstallSignal = {
212
- signal_type: 'framework_install',
213
- install_serial: serialData.serial_number,
214
- package: 'spine-framework',
215
- version: serialData.framework_version,
191
+ const signal = {
192
+ stage: 'installed' as const,
193
+ source: 'use' as const,
194
+ action_type: 'framework_install',
195
+ action_value: 2 as const,
196
+ instance_id: serialData.serial_number,
216
197
  anonymous_id: `anon_${randomBytes(8).toString('hex')}`,
217
- machine_fingerprint: serialData.machine_fingerprint,
218
- platform: `${process.platform}-${process.arch}`,
219
- timestamp: new Date().toISOString(),
198
+ environment: (process.env.NODE_ENV === 'production' ? 'production' : 'dev') as 'dev' | 'production',
199
+ occurred_at: new Date().toISOString(),
220
200
  }
221
201
 
222
202
  try {
223
- await fetch(`${signalConfig.endpoint}/api/integrations/webhook/funnel-signal`, {
203
+ await fetch(`${signalConfig.endpoint}/api/signal`, {
224
204
  method: 'POST',
225
205
  headers: { 'Content-Type': 'application/json' },
226
206
  body: JSON.stringify(signal),
@@ -230,20 +210,23 @@ export async function sendFrameworkInstallSignal(): Promise<void> {
230
210
  }
231
211
  }
232
212
 
233
- export async function sendAppInstallSignal(packageName: string, version: string): Promise<void> {
213
+ export async function sendAppInstallSignal(packageName: string, _version: string): Promise<void> {
234
214
  const serialData = ensureSerialData()
235
215
 
236
- const signal: AppInstallSignal = {
237
- signal_type: 'app_install',
238
- package: packageName,
239
- version,
240
- framework_install_serial: serialData.serial_number,
241
- app_serial: `${packageName}_${randomBytes(8).toString('hex')}`,
242
- timestamp: new Date().toISOString(),
216
+ const signal = {
217
+ stage: 'installed' as const,
218
+ source: 'use' as const,
219
+ action_type: 'app_install',
220
+ action_value: 2 as const,
221
+ instance_id: serialData.serial_number,
222
+ anonymous_id: `anon_${randomBytes(8).toString('hex')}`,
223
+ action_description: packageName,
224
+ environment: (process.env.NODE_ENV === 'production' ? 'production' : 'dev') as 'dev' | 'production',
225
+ occurred_at: new Date().toISOString(),
243
226
  }
244
227
 
245
228
  try {
246
- await fetch(`${signalConfig.endpoint}/api/integrations/webhook/funnel-signal`, {
229
+ await fetch(`${signalConfig.endpoint}/api/signal`, {
247
230
  method: 'POST',
248
231
  headers: { 'Content-Type': 'application/json' },
249
232
  body: JSON.stringify(signal),
@@ -198,13 +198,31 @@ async function upsertTriggers(appId: string, records: any[], dryRun: boolean): P
198
198
  }
199
199
  }
200
200
 
201
+ // Resolve pipeline_slug → pipeline_id for trigger→pipeline wiring
202
+ let pipelineId: string | undefined = record.pipeline_id
203
+ if (!pipelineId && record.pipeline_slug) {
204
+ const { data: pipelineData, error: pipelineError } = await adminDb
205
+ .from('pipelines')
206
+ .select('id')
207
+ .eq('slug', record.pipeline_slug)
208
+ .eq('app_id', appId)
209
+ .maybeSingle()
210
+
211
+ if (pipelineError || !pipelineData) {
212
+ result.errors.push(`${record.name}: pipeline_slug '${record.pipeline_slug}' not found — trigger will be inactive until pipeline is created`)
213
+ } else {
214
+ pipelineId = pipelineData.id
215
+ }
216
+ }
217
+
218
+ // Build the row — strip pipeline_slug (not a db column), inject resolved pipeline_id
219
+ const { pipeline_slug: _pslug, ...recordWithoutPipelineSlug } = record
220
+ const triggerRow = { ...recordWithoutPipelineSlug, app_id: appId, config }
221
+ if (pipelineId) triggerRow.pipeline_id = pipelineId
222
+
201
223
  const { error } = await adminDb
202
224
  .from('triggers')
203
- .upsert({
204
- ...record,
205
- app_id: appId,
206
- config,
207
- }, {
225
+ .upsert(triggerRow, {
208
226
  onConflict: 'app_id,name',
209
227
  ignoreDuplicates: false
210
228
  })
@@ -270,18 +288,21 @@ async function upsertPipelines(appId: string, records: any[], dryRun: boolean):
270
288
 
271
289
  for (const record of records) {
272
290
  if (dryRun) {
273
- console.log(` [dry-run] Would upsert pipeline: ${record.slug}`)
291
+ console.log(` [dry-run] Would upsert pipeline: ${record.slug || record.name}`)
274
292
  result.inserted++
275
293
  continue
276
294
  }
277
295
 
296
+ // Prefer slug-based conflict resolution; fall back to name if no slug
297
+ const conflictKey = record.slug ? 'app_id,slug' : 'app_id,name'
298
+
278
299
  const { error } = await adminDb
279
300
  .from('pipelines')
280
301
  .upsert({
281
302
  ...record,
282
303
  app_id: appId,
283
304
  }, {
284
- onConflict: 'app_id,name',
305
+ onConflict: conflictKey,
285
306
  ignoreDuplicates: false
286
307
  })
287
308
 
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @module config/signal-client
3
+ * @audience installer
4
+ * @layer cli
5
+ * @stability stable
6
+ *
7
+ * Universal funnel signal client — browser, Node.js, and Deno compatible.
8
+ * Sends signals to the public `/api/signal` endpoint (no auth required).
9
+ *
10
+ * Usage (marketing site):
11
+ * ```ts
12
+ * import { SpineSignal } from 'spine-framework/config/signal-client'
13
+ * SpineSignal.init({ endpoint: 'https://cortex.yourcompany.com' })
14
+ * SpineSignal.send({ stage: 'anonymous', source: 'mar', action_type: 'page_view', action_value: 1, session_id: sid })
15
+ * ```
16
+ *
17
+ * Usage (pixel — email tracking):
18
+ * ```html
19
+ * <img src="https://cortex.yourcompany.com/api/signal?format=pixel&stage=identified&source=mar&action_type=email_open&action_value=1&person_id=UUID" width="1" height="1" />
20
+ * ```
21
+ *
22
+ * Usage (CLI / Node.js):
23
+ * ```ts
24
+ * import { SpineSignal } from 'spine-framework/config/signal-client'
25
+ * await SpineSignal.send({ stage: 'installed', source: 'use', action_type: 'framework_init', action_value: 2, instance_id: serial })
26
+ * ```
27
+ */
28
+
29
+ export interface SignalPayload {
30
+ stage: 'anonymous' | 'identified' | 'installed'
31
+ source: 'mar' | 'int' | 'use' | 'manual'
32
+ action_type: string
33
+ action_value: 1 | 2 | 5
34
+ pipeline_slug?: string
35
+ anonymous_id?: string
36
+ person_id?: string
37
+ account_id?: string
38
+ session_id?: string
39
+ action_description?: string
40
+ occurred_at?: string
41
+ url?: string
42
+ path?: string
43
+ referrer?: string
44
+ user_agent?: string
45
+ utm_source?: string
46
+ utm_medium?: string
47
+ utm_campaign?: string
48
+ instance_id?: string
49
+ environment?: 'dev' | 'staging' | 'production'
50
+ }
51
+
52
+ export interface SignalClientConfig {
53
+ endpoint: string
54
+ timeout?: number
55
+ }
56
+
57
+ export interface SignalResult {
58
+ ok: boolean
59
+ signal_id?: string
60
+ rating?: number
61
+ error?: string
62
+ }
63
+
64
+ let _config: SignalClientConfig = {
65
+ endpoint: 'https://cortex.spine-framework.com',
66
+ timeout: 5000,
67
+ }
68
+
69
+ /**
70
+ * Initialize the signal client with a custom endpoint.
71
+ * Call once at app startup before any `send()` calls.
72
+ */
73
+ function init(config: SignalClientConfig): void {
74
+ _config = { ..._config, ...config }
75
+ }
76
+
77
+ /**
78
+ * Send a funnel signal. Fire-and-forget safe — never throws.
79
+ * Returns a result object indicating success or failure.
80
+ */
81
+ async function send(payload: SignalPayload): Promise<SignalResult> {
82
+ try {
83
+ const controller = new AbortController()
84
+ const timeout = setTimeout(() => controller.abort(), _config.timeout ?? 5000)
85
+
86
+ const response = await fetch(`${_config.endpoint}/api/signal`, {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify(payload),
90
+ signal: controller.signal,
91
+ }).finally(() => clearTimeout(timeout))
92
+
93
+ if (!response.ok) {
94
+ const body = await response.json().catch(() => ({}))
95
+ return { ok: false, error: body?.error || `HTTP ${response.status}` }
96
+ }
97
+
98
+ const data = await response.json()
99
+ return {
100
+ ok: true,
101
+ signal_id: data?.data?.signal_id,
102
+ rating: data?.data?.rating,
103
+ }
104
+ } catch {
105
+ return { ok: false, error: 'Signal send failed (network or timeout)' }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Build a tracking pixel URL for email templates.
111
+ * Embed as: `<img src={SpineSignal.pixelUrl({...})} width="1" height="1" />`
112
+ */
113
+ function pixelUrl(payload: Omit<SignalPayload, 'session_id'>): string {
114
+ const params = new URLSearchParams({ format: 'pixel' })
115
+ for (const [key, value] of Object.entries(payload)) {
116
+ if (value !== undefined && value !== null) {
117
+ params.set(key, String(value))
118
+ }
119
+ }
120
+ return `${_config.endpoint}/api/signal?${params.toString()}`
121
+ }
122
+
123
+ /**
124
+ * Minimal browser session ID — stable for the tab session.
125
+ * Suitable for `session_id` on marketing source signals.
126
+ * Returns a consistent ID stored in sessionStorage (browser only).
127
+ */
128
+ function getSessionId(): string {
129
+ if (typeof sessionStorage !== 'undefined') {
130
+ const key = '_spine_sid'
131
+ let sid = sessionStorage.getItem(key)
132
+ if (!sid) {
133
+ sid = `sess_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
134
+ sessionStorage.setItem(key, sid)
135
+ }
136
+ return sid
137
+ }
138
+ // Node.js / non-browser: generate ephemeral ID
139
+ return `sess_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
140
+ }
141
+
142
+ export const SpineSignal = {
143
+ init,
144
+ send,
145
+ pixelUrl,
146
+ getSessionId,
147
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @module signal
3
+ * @audience both
4
+ * @layer api-handler
5
+ * @stability stable
6
+ *
7
+ * Public funnel signal ingestion endpoint. Accepts signals from any source
8
+ * (browser JS, CLI, email pixel) without authentication. Rate limiting is
9
+ * enforced at the Netlify edge layer via netlify.toml.
10
+ *
11
+ * **Routed by:** `POST /.netlify/functions/signal`
12
+ * `GET /.netlify/functions/signal?format=pixel`
13
+ *
14
+ * **Request formats:**
15
+ * - `POST` with JSON body — standard signal (returns JSON response)
16
+ * - `GET ?format=pixel` — tracking pixel (returns 1×1 transparent GIF)
17
+ *
18
+ * **Required fields (POST body / GET query params):**
19
+ * - `stage` — 'anonymous' | 'identified' | 'installed'
20
+ * - `source` — 'mar' | 'int' | 'use' | 'manual'
21
+ * - `action_type` — string (e.g. 'page_view', 'form_submit')
22
+ * - `action_value` — 1 | 2 | 5
23
+ * - at least one of: `anonymous_id`, `person_id`, `account_id`
24
+ * - `session_id` — required when source = 'mar'
25
+ *
26
+ * **Optional fields:**
27
+ * - `pipeline_slug` — routes signal to a specific funnel pipeline
28
+ * - `path`, `referrer`, `url`, `utm_*`, `instance_id`, `environment`
29
+ *
30
+ * **Authorization:** None — intentionally public. Signals are best-effort and
31
+ * validated by `processSignal` in `custom_funnel-signal.ts`.
32
+ *
33
+ * **CORS:** Allows all origins for cross-origin browser calls.
34
+ *
35
+ * @seeAlso custom/apps/cortex/functions/custom_funnel-signal.ts (processSignal)
36
+ * @seeAlso config/signal-client.ts (browser/Node client for senders)
37
+ */
38
+
39
+ // 1×1 transparent GIF (base64)
40
+ const PIXEL_GIF_BASE64 = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
41
+ const PIXEL_GIF = Buffer.from(PIXEL_GIF_BASE64, 'base64')
42
+
43
+ const CORS_HEADERS = {
44
+ 'Access-Control-Allow-Origin': '*',
45
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
46
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Session-Id',
47
+ 'Access-Control-Max-Age': '86400',
48
+ }
49
+
50
+ export const handler = async (event: any) => {
51
+ // Handle CORS preflight
52
+ if (event.httpMethod === 'OPTIONS') {
53
+ return { statusCode: 204, headers: CORS_HEADERS, body: '' }
54
+ }
55
+
56
+ const requestId = crypto.randomUUID()
57
+ const isPixel = event.queryStringParameters?.format === 'pixel'
58
+
59
+ try {
60
+ // Build signal payload from body (POST) or query params (GET pixel)
61
+ let signalData: any
62
+
63
+ if (event.httpMethod === 'GET' && isPixel) {
64
+ signalData = { ...event.queryStringParameters }
65
+ // action_value comes as string in query params — coerce to number
66
+ if (signalData.action_value) {
67
+ signalData.action_value = parseInt(signalData.action_value, 10)
68
+ }
69
+ } else if (event.httpMethod === 'POST') {
70
+ if (!event.body) {
71
+ return {
72
+ statusCode: 400,
73
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ error: 'Missing request body' }),
75
+ }
76
+ }
77
+ try {
78
+ signalData = JSON.parse(event.body)
79
+ } catch {
80
+ return {
81
+ statusCode: 400,
82
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ error: 'Invalid JSON body' }),
84
+ }
85
+ }
86
+ } else {
87
+ return {
88
+ statusCode: 405,
89
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ error: 'Method not allowed' }),
91
+ }
92
+ }
93
+
94
+ // Resolve processSignal dynamically — core must not statically import custom handlers.
95
+ // After assembly, custom_funnel-signal is a sibling in the same functions directory.
96
+ let processSignal: Function
97
+ try {
98
+ // @ts-ignore — custom_funnel-signal is injected at runtime by the assembly step
99
+ const mod = await import('./custom_funnel-signal')
100
+ processSignal = mod.processSignal
101
+ if (typeof processSignal !== 'function') throw new Error('processSignal not exported')
102
+ } catch {
103
+ return {
104
+ statusCode: 503,
105
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ error: 'Signal handler not available' }),
107
+ }
108
+ }
109
+
110
+ // Minimal script context for processSignal
111
+ const scriptContext = {
112
+ requestId,
113
+ accountId: null,
114
+ slug: 'public-signal',
115
+ principal: { id: 'system', type: 'machine', accountId: null },
116
+ headers: event.headers || {},
117
+ }
118
+
119
+ const scriptEvent = {
120
+ httpMethod: event.httpMethod,
121
+ headers: event.headers || {},
122
+ body: signalData,
123
+ path: event.path || '/signal',
124
+ queryStringParameters: event.queryStringParameters || {},
125
+ }
126
+
127
+ // Fire-and-forget for pixel requests; await for JSON
128
+ if (isPixel) {
129
+ processSignal(signalData, scriptContext, scriptEvent).catch(() => {
130
+ // Silent fail for pixel — response is always the GIF
131
+ })
132
+ return {
133
+ statusCode: 200,
134
+ headers: {
135
+ ...CORS_HEADERS,
136
+ 'Content-Type': 'image/gif',
137
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
138
+ 'Pragma': 'no-cache',
139
+ },
140
+ body: PIXEL_GIF.toString('base64'),
141
+ isBase64Encoded: true,
142
+ }
143
+ }
144
+
145
+ const result = await processSignal(signalData, scriptContext, scriptEvent)
146
+
147
+ if (result.status === 'error') {
148
+ return {
149
+ statusCode: 400,
150
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({ error: result.error }),
152
+ }
153
+ }
154
+
155
+ return {
156
+ statusCode: 200,
157
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({ data: result }),
159
+ }
160
+
161
+ } catch (err: any) {
162
+ return {
163
+ statusCode: 500,
164
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ error: 'Internal error' }),
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,17 @@
1
+ -- Migration 005: Add slug to pipelines, rename steps → stages
2
+ -- Adds a human-readable slug for pipeline identification and seed-time resolution.
3
+ -- Renames the `steps` column to `stages` to match pipeline-runner.ts expectations.
4
+
5
+ -- Add slug column (nullable initially so existing rows don't break)
6
+ ALTER TABLE public.pipelines
7
+ ADD COLUMN IF NOT EXISTS slug text;
8
+
9
+ -- Rename steps → stages to match pipeline-runner.ts (reads pipeline.stages)
10
+ ALTER TABLE public.pipelines
11
+ RENAME COLUMN steps TO stages;
12
+
13
+ -- Add unique constraint on (app_id, slug) for seed-time resolution
14
+ -- Partial index: only enforce uniqueness where slug IS NOT NULL
15
+ CREATE UNIQUE INDEX IF NOT EXISTS pipelines_app_id_slug_idx
16
+ ON public.pipelines (app_id, slug)
17
+ WHERE slug IS NOT NULL;
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.3.11] — 2026-06-09
8
+ - Added `updateAccountFunnel` per-pipeline_slug rating storage in `custom_funnel-signal.ts` — signals now write `ratings[pipeline_slug]` alongside `ratings[stage]`, enabling per-funnel score breakdown in the UI
9
+
10
+ ## [0.3.10] — 2026-06-08
11
+ - Updated `LICENSE.md` to full Spine Framework Internal Use License 1.0.0 (23 sections)
12
+
13
+ ## [0.3.9] — 2026-06-08
14
+ - Added `CHANGELOG.md` to all three packages
15
+ - Added `CHANGELOG.md` to files copied by `npx spine-framework init`
16
+
7
17
  ## [0.3.8] — 2026-06-08
8
18
  - Added `README.md` and `LICENSE.md` to files copied by `npx spine-framework init`
9
19
  - Fixed duplicate `app_installations` table definition in `000_foundation.sql` — the incomplete first definition was winning the `CREATE TABLE IF NOT EXISTS` race, resulting in missing columns (`package_name`, `installed_version`) and no `UNIQUE` constraint; this caused `⚠️ Failed to record installation` on every `install-app` run
package/LICENSE.md CHANGED
@@ -1,18 +1,223 @@
1
1
  # Spine Framework Internal Use License 1.0.0
2
2
 
3
- **Source-available. Free for internal use. Commercial rights reserved.**
3
+ Source-available. Free for internal use. Commercial rights reserved.
4
4
 
5
- Copyright © 2026 Dahl Ventures Inc. All rights reserved.
5
+ This license is based on the structure and intent of the PolyForm Internal Use License 1.0.0, but it has been modified for Spine Framework. This is not the unmodified PolyForm Internal Use License.
6
6
 
7
- This license is based on the PolyForm Internal Use License 1.0.0, modified for Spine Framework.
7
+ ## Required Notice
8
8
 
9
- ## Quick Summary
9
+ Required Notice: Copyright © 2026 Dahl Ventures Inc. All rights reserved.
10
10
 
11
- **You CAN:** Use Spine Framework inside your company for internal business operations.
12
- **You CANNOT:** Sell it, offer it as SaaS, white-label it, distribute it, or use it to build competing products without a Commercial License.
11
+ ## 1. Acceptance
13
12
 
14
- ## Full Terms
13
+ In order to receive any license under these terms, you must agree to these terms as both strict obligations and conditions to all licenses granted to you.
15
14
 
16
- See the complete license text at: https://spine-framework.com/license
15
+ If you do not agree to these terms, you do not have permission to use, copy, modify, run, install, access, or create works based on Spine Framework.
17
16
 
18
- For Commercial Licensing inquiries: webmaster@spine-framework.com
17
+ ## 2. Definitions
18
+
19
+ **Software** means Spine Framework, including its source code, object code, packages, modules, templates, schemas, migrations, configuration files, documentation, examples, command-line tools, SDKs, application packs, and related materials provided by the licensor.
20
+
21
+ **Licensor** means the legal owner of Spine Framework.
22
+
23
+ **You** means the individual or legal entity receiving the Software.
24
+
25
+ **Your Company** means the legal entity on whose behalf you use the Software, including that entity's employees and authorized contractors.
26
+
27
+ **Internal Business Use** means use of the Software solely inside Your Company for Your Company's own internal business operations, internal workflows, internal applications, internal data, internal users, and internal customers or accounts.
28
+
29
+ **Third Party** means any person or legal entity other than You or Your Company.
30
+
31
+ **Commercial License** means a separate written agreement signed by the Licensor that grants rights beyond this license.
32
+
33
+ ## 3. Copyright License
34
+
35
+ The Licensor grants You a copyright license to do everything with the Software that would otherwise infringe the Licensor's copyright, but only for Internal Business Use and only as permitted by these terms.
36
+
37
+ You may install, run, access, evaluate, test, configure, and use the Software for Internal Business Use.
38
+
39
+ You may copy the Software only as reasonably necessary for Internal Business Use, including backup, deployment, development, testing, staging, disaster recovery, and internal security review.
40
+
41
+ You may not distribute the Software except as expressly allowed in these terms.
42
+
43
+ ## 4. Changes and New Works
44
+
45
+ The Licensor grants You a copyright license to modify the Software and create new works based on the Software, but only for Internal Business Use.
46
+
47
+ You may create internal applications, extensions, workflows, integrations, automations, schemas, roles, agents, triggers, pipelines, dashboards, reports, and other internal business systems using the Software.
48
+
49
+ You may not distribute, sell, sublicense, transfer, lease, rent, host for others, white-label, resell, or commercially exploit any modified version, derivative work, extension, application pack, template, or implementation of the Software except under a Commercial License.
50
+
51
+ ## 5. Internal Contractors
52
+
53
+ Your Company may allow employees and contractors to access and use the Software solely on behalf of Your Company and solely for Your Company's Internal Business Use.
54
+
55
+ Contractors do not receive any independent right to use the Software for themselves, their other clients, their own products, or any Third Party.
56
+
57
+ Your Company is responsible for all use of the Software by its employees, contractors, agents, and representatives.
58
+
59
+ ## 6. Your Data and Application-Specific Work
60
+
61
+ As between You and the Licensor, You own Your Company's data, records, content, prompts, business logic, customer records, internal workflows, and application-specific code that You independently create using the Software.
62
+
63
+ These terms do not give the Licensor ownership of Your Company's data or independently created application-specific business content.
64
+
65
+ However, ownership of Your Company's data or application-specific work does not give You the right to distribute, resell, white-label, host, or commercially exploit the Software or any portion of the Software except as allowed by these terms or a Commercial License.
66
+
67
+ ## 7. Prohibited Uses
68
+
69
+ You may not do any of the following without a Commercial License:
70
+
71
+ - Sell, resell, sublicense, rent, lease, lend, transfer, assign, publish, or distribute the Software.
72
+ - Offer the Software, or any modified version of the Software, to Third Parties as software, infrastructure, a framework, a platform, a service, or a hosted product.
73
+ - Offer the Software as SaaS, PaaS, IaaS, framework-as-a-service, backend-as-a-service, workflow-as-a-service, automation-as-a-service, AI-agent platform, managed service, support service, or similar commercial service.
74
+ - White-label, private-label, rebrand, or remove Spine Framework identity from the Software in order to provide it to Third Parties.
75
+ - Embed, bundle, package, or include the Software in any product, service, appliance, marketplace offering, application pack, starter kit, template library, SDK, CLI, distribution, or managed implementation made available to Third Parties.
76
+ - Use the Software to operate, power, support, or provide a product or service for Third Parties where those Third Parties receive access to the Software or to functionality substantially provided by the Software.
77
+ - Use the Software to build, operate, or improve a competing framework, platform, marketplace, application generator, agentic software infrastructure product, or commercial development infrastructure product.
78
+ - Provide implementation, consulting, hosting, support, customization, training, migration, or managed services for the Software to Third Parties as a paid or commercial offering, except as expressly authorized by the Licensor.
79
+ - Remove, alter, obscure, or bypass copyright notices, license notices, attribution, trademarks, branding, license keys, license checks, usage limits, or technical protection measures included in the Software.
80
+ - Use the Software in violation of law, regulation, third-party rights, or these terms.
81
+
82
+ ## 8. Examples of Permitted Use
83
+
84
+ The following are permitted under this license, provided they are solely for Internal Business Use:
85
+
86
+ - A company installs Spine Framework to build its own internal CRM.
87
+ - A company uses Spine Framework to operate an internal customer support portal for its own support team and its own customers.
88
+ - A company modifies Spine Framework for its internal workflows, roles, permissions, automations, pipelines, and integrations.
89
+ - A company lets its employees and authorized contractors configure, maintain, and support its internal Spine Framework installation.
90
+ - A company creates internal dashboards, agents, knowledge bases, audit trails, and operational tools on top of Spine Framework.
91
+
92
+ ## 9. Examples Requiring a Commercial License
93
+
94
+ The following are not permitted under this license and require a Commercial License:
95
+
96
+ - Selling Spine Framework or a modified version of Spine Framework.
97
+ - Offering Spine Framework as a hosted SaaS product.
98
+ - Offering a white-labeled application built on Spine Framework to customers.
99
+ - Packaging Spine Framework into a commercial starter kit, template, marketplace app, agency offering, or developer platform.
100
+ - Running Spine Framework for multiple clients as a managed service provider, agency, consultant, systems integrator, or outsourcing firm.
101
+ - Embedding Spine Framework into a product sold to customers.
102
+ - Forking Spine Framework to launch a competing framework, backend, app platform, AI-agent platform, or development infrastructure product.
103
+ - Publishing modified Spine Framework source code to a public repository.
104
+ - Giving a customer, client, partner, reseller, or affiliate a copy of Spine Framework.
105
+
106
+ ## 10. Distribution
107
+
108
+ You may not distribute the Software, modified versions of the Software, or works based on the Software to any Third Party except as expressly permitted by these terms or a Commercial License.
109
+
110
+ You may share copies of the Software with employees and contractors of Your Company only as necessary for Internal Business Use and only if they are bound by obligations consistent with these terms.
111
+
112
+ Any copy of the Software must include this license and all Required Notices.
113
+
114
+ ## 11. Trademarks
115
+
116
+ This license does not grant You any right to use the Licensor's names, trademarks, service marks, logos, product names, domain names, or branding except as necessary to identify the Software for Internal Business Use.
117
+
118
+ You may not use Spine Framework branding in a way that suggests sponsorship, endorsement, partnership, certification, resale authorization, or commercial authorization without written permission from the Licensor.
119
+
120
+ ## 12. Patent License
121
+
122
+ The Licensor grants You a patent license for the Software that covers patent claims the Licensor can license, or later becomes able to license, that You would infringe by using the Software as permitted by these terms.
123
+
124
+ ## 13. Patent Defense
125
+
126
+ If You make any written claim that the Software infringes or contributes to infringement of any patent, Your patent license for the Software ends immediately.
127
+
128
+ If Your Company makes such a claim, Your Company's patent license ends immediately.
129
+
130
+ ## 14. Fair Use and Other Legal Rights
131
+
132
+ You may have fair use, fair dealing, or other rights under applicable law. These terms do not limit rights that the law does not allow these terms to limit.
133
+
134
+ ## 15. No Other Rights
135
+
136
+ These terms do not allow You to sublicense, transfer, assign, or delegate any license granted under these terms except as expressly allowed here.
137
+
138
+ These terms do not prevent the Licensor from granting licenses to anyone else.
139
+
140
+ These terms do not imply any license other than the licenses expressly granted here.
141
+
142
+ All rights not expressly granted are reserved by the Licensor.
143
+
144
+ ## 16. Violations and Cure
145
+
146
+ The first time You are notified in writing that You have violated these terms, or done anything with the Software not covered by these terms, Your licenses can continue if You:
147
+
148
+ - come into full compliance with these terms;
149
+ - take practical steps to correct past violations; and
150
+ - complete both within 32 days of receiving written notice.
151
+
152
+ If You do not satisfy those requirements, all licenses granted to You under these terms end immediately.
153
+
154
+ For any later violation, all licenses granted to You under these terms end immediately without any cure period.
155
+
156
+ ## 17. Termination
157
+
158
+ When Your licenses end, You must stop all use of the Software and destroy all copies of the Software in Your possession or control, except copies You are legally required to retain for archival, compliance, or legal purposes.
159
+
160
+ Sections concerning ownership, prohibited uses, trademarks, patent defense, no other rights, violations, termination, no warranty, limitation of liability, and general terms survive termination.
161
+
162
+ ## 18. No Warranty
163
+
164
+ As far as the law allows, the Software is provided as is and as available, without any warranty or condition, express or implied.
165
+
166
+ The Licensor disclaims all warranties and conditions, including warranties of merchantability, fitness for a particular purpose, title, non-infringement, availability, accuracy, security, and quiet enjoyment.
167
+
168
+ ## 19. Limitation of Liability
169
+
170
+ As far as the law allows, the Licensor will not be liable to You for any indirect, incidental, special, consequential, exemplary, punitive, or other damages arising out of these terms or the Software.
171
+
172
+ As far as the law allows, the Licensor will not be liable for lost profits, lost revenue, lost savings, lost data, business interruption, security incidents, procurement of substitute services, or loss of goodwill.
173
+
174
+ The Licensor's total liability arising out of these terms or the Software will not exceed the amount You paid the Licensor for the Software during the 12 months before the event giving rise to liability, or USD $100 if You paid nothing.
175
+
176
+ ## 20. Commercial Licensing
177
+
178
+ Rights not granted under this license may be available under a separate Commercial License from the Licensor.
179
+
180
+ A Commercial License is required for resale, redistribution, SaaS, managed services, white-label use, OEM use, marketplace distribution, partner use, commercial hosting, commercial support offerings, or any use outside Internal Business Use.
181
+
182
+ To request commercial rights, contact:
183
+
184
+ - spine-framework.com
185
+ - webmaster@spine-framework.com
186
+
187
+ ## 21. License Updates
188
+
189
+ The Licensor may publish updated versions of this license from time to time.
190
+
191
+ Each release, package, download, or copy of the Software is governed by the license terms included with that release, package, download, or copy, unless You and the Licensor have signed a separate written agreement.
192
+
193
+ Updated license terms apply only to future releases, downloads, packages, updates, upgrades, patches, hosted access, or other versions of the Software made available after the updated terms are published.
194
+
195
+ The Licensor may require acceptance of updated license terms as a condition of receiving or using future releases, updates, hosted services, commercial features, support, documentation, marketplace access, license keys, portals, application packs, or other Spine Framework services.
196
+
197
+ Nothing in this section gives You rights to continue using future versions of the Software under earlier license terms.
198
+
199
+ ## 22. General Terms
200
+
201
+ If any part of these terms is held unenforceable, the rest remains in effect as far as the law allows.
202
+
203
+ Failure by the Licensor to enforce any term is not a waiver.
204
+
205
+ Any waiver must be in writing and signed by the Licensor.
206
+
207
+ These terms are the entire license terms for the Software unless You and the Licensor have signed a separate written agreement.
208
+
209
+ ## 23. Plain-English Summary
210
+
211
+ *This summary is not part of the legal license.*
212
+
213
+ You may use Spine Framework inside your own company for your own internal business operations.
214
+
215
+ You may modify Spine Framework for your own internal use.
216
+
217
+ You may let your employees and contractors work on your internal Spine Framework installation.
218
+
219
+ You may not sell it, redistribute it, white-label it, host it for others, offer it as SaaS, package it into another product, use it as a managed service offering, or use it to build a competing framework without a separate commercial license.
220
+
221
+ Future versions, updates, services, support, portals, packs, license keys, and commercial features may require acceptance of updated terms.
222
+
223
+ Spine Framework is source-available. It is not open source.
@@ -32,25 +32,7 @@ export declare function readEngagementData(): EngagementData | null;
32
32
  export declare function writeEngagementData(data: EngagementData): void;
33
33
  export declare function recordEngagementSignal(signalType: EngagementSignalType): void;
34
34
  export declare function shouldOfferClaim(): ClaimPrompt | null;
35
- export interface FrameworkInstallSignal {
36
- signal_type: 'framework_install';
37
- install_serial: string;
38
- package: string;
39
- version: string;
40
- anonymous_id: string;
41
- machine_fingerprint: string;
42
- platform: string;
43
- timestamp: string;
44
- }
45
- export interface AppInstallSignal {
46
- signal_type: 'app_install';
47
- package: string;
48
- version: string;
49
- framework_install_serial: string;
50
- app_serial: string;
51
- timestamp: string;
52
- }
53
35
  export declare function sendFrameworkInstallSignal(): Promise<void>;
54
- export declare function sendAppInstallSignal(packageName: string, version: string): Promise<void>;
36
+ export declare function sendAppInstallSignal(packageName: string, _version: string): Promise<void>;
55
37
  export declare function registerClaimCommands(program: Command): void;
56
38
  //# sourceMappingURL=claim.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/claim.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKxC,OAAO,EAAkD,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAS1J,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,CAAA;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAiBD,wBAAgB,gBAAgB,IAAI,UAAU,CA6B7C;AAeD,wBAAgB,kBAAkB,IAAI,cAAc,GAAG,IAAI,CAO1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAK9D;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,oBAAoB,GAAG,IAAI,CAgC7E;AAED,wBAAgB,gBAAgB,IAAI,WAAW,GAAG,IAAI,CA8BrD;AAID,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,mBAAmB,CAAA;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,aAAa,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,wBAAwB,EAAE,MAAM,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAuBhE;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB9F;AAkLD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAe5D"}
1
+ {"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/claim.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKxC,OAAO,EAAkD,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAS1J,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,CAAA;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAiBD,wBAAgB,gBAAgB,IAAI,UAAU,CA6B7C;AAeD,wBAAgB,kBAAkB,IAAI,cAAc,GAAG,IAAI,CAO1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAK9D;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,oBAAoB,GAAG,IAAI,CAgC7E;AAED,wBAAgB,gBAAgB,IAAI,WAAW,GAAG,IAAI,CA8BrD;AAID,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAuBhE;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB/F;AAkLD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAe5D"}
@@ -1 +1 @@
1
- {"version":3,"file":"install-app.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/install-app.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAmkBxC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,QAgB1D"}
1
+ {"version":3,"file":"install-app.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/install-app.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAwlBxC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,QAgB1D"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module signal
3
+ * @audience both
4
+ * @layer api-handler
5
+ * @stability stable
6
+ *
7
+ * Public funnel signal ingestion endpoint. Accepts signals from any source
8
+ * (browser JS, CLI, email pixel) without authentication. Rate limiting is
9
+ * enforced at the Netlify edge layer via netlify.toml.
10
+ *
11
+ * **Routed by:** `POST /.netlify/functions/signal`
12
+ * `GET /.netlify/functions/signal?format=pixel`
13
+ *
14
+ * **Request formats:**
15
+ * - `POST` with JSON body — standard signal (returns JSON response)
16
+ * - `GET ?format=pixel` — tracking pixel (returns 1×1 transparent GIF)
17
+ *
18
+ * **Required fields (POST body / GET query params):**
19
+ * - `stage` — 'anonymous' | 'identified' | 'installed'
20
+ * - `source` — 'mar' | 'int' | 'use' | 'manual'
21
+ * - `action_type` — string (e.g. 'page_view', 'form_submit')
22
+ * - `action_value` — 1 | 2 | 5
23
+ * - at least one of: `anonymous_id`, `person_id`, `account_id`
24
+ * - `session_id` — required when source = 'mar'
25
+ *
26
+ * **Optional fields:**
27
+ * - `pipeline_slug` — routes signal to a specific funnel pipeline
28
+ * - `path`, `referrer`, `url`, `utm_*`, `instance_id`, `environment`
29
+ *
30
+ * **Authorization:** None — intentionally public. Signals are best-effort and
31
+ * validated by `processSignal` in `custom_funnel-signal.ts`.
32
+ *
33
+ * **CORS:** Allows all origins for cross-origin browser calls.
34
+ *
35
+ * @seeAlso custom/apps/cortex/functions/custom_funnel-signal.ts (processSignal)
36
+ * @seeAlso config/signal-client.ts (browser/Node client for senders)
37
+ */
38
+ export declare const handler: (event: any) => Promise<{
39
+ statusCode: number;
40
+ headers: {
41
+ 'Access-Control-Allow-Origin': string;
42
+ 'Access-Control-Allow-Methods': string;
43
+ 'Access-Control-Allow-Headers': string;
44
+ 'Access-Control-Max-Age': string;
45
+ };
46
+ body: string;
47
+ isBase64Encoded?: undefined;
48
+ } | {
49
+ statusCode: number;
50
+ headers: {
51
+ 'Content-Type': string;
52
+ 'Access-Control-Allow-Origin': string;
53
+ 'Access-Control-Allow-Methods': string;
54
+ 'Access-Control-Allow-Headers': string;
55
+ 'Access-Control-Max-Age': string;
56
+ };
57
+ body: string;
58
+ isBase64Encoded?: undefined;
59
+ } | {
60
+ statusCode: number;
61
+ headers: {
62
+ 'Content-Type': string;
63
+ 'Cache-Control': string;
64
+ Pragma: string;
65
+ 'Access-Control-Allow-Origin': string;
66
+ 'Access-Control-Allow-Methods': string;
67
+ 'Access-Control-Allow-Headers': string;
68
+ 'Access-Control-Max-Age': string;
69
+ };
70
+ body: string;
71
+ isBase64Encoded: boolean;
72
+ }>;
73
+ //# sourceMappingURL=signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../.framework/functions/signal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAaH,eAAO,MAAM,OAAO,UAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsHvC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "private": false,
5
5
  "description": "Spine — enterprise application framework built on Supabase + Netlify + React",
6
6
  "type": "module",
@@ -28,8 +28,10 @@ fi
28
28
  echo ""
29
29
 
30
30
  # Check 2: Core importing custom_ prefixed modules
31
+ # signal.ts is excluded: it uses a guarded dynamic import to optionally load a custom handler at runtime.
31
32
  echo "2. Checking for custom_ module imports in core..."
32
- if grep -r "from.*custom_\|import.*custom_" .framework/ --include="*.ts" --include="*.tsx" 2>/dev/null; then
33
+ if grep -r "from.*custom_\|import.*custom_" .framework/ --include="*.ts" --include="*.tsx" 2>/dev/null \
34
+ | grep -v "signal\.ts:"; then
33
35
  echo " ❌ FAIL: Core files import custom_ modules"
34
36
  VIOLATIONS=$((VIOLATIONS + 1))
35
37
  else