@secondlayer/shared 13.11.0 → 13.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.
Files changed (41) hide show
  1. package/dist/src/archive/range-digest.d.ts +125 -0
  2. package/dist/src/archive/semantic-digest-builder.d.ts +125 -0
  3. package/dist/src/coverage/index.d.ts +125 -0
  4. package/dist/src/db/index.d.ts +146 -2
  5. package/dist/src/db/index.js +11 -1
  6. package/dist/src/db/index.js.map +3 -3
  7. package/dist/src/db/queries/chain-reorgs.d.ts +125 -0
  8. package/dist/src/db/queries/chain-reorgs.js +11 -1
  9. package/dist/src/db/queries/chain-reorgs.js.map +3 -3
  10. package/dist/src/db/queries/contracts.d.ts +125 -0
  11. package/dist/src/db/queries/instance.d.ts +125 -0
  12. package/dist/src/db/queries/integrity.d.ts +125 -0
  13. package/dist/src/db/queries/subgraph-gaps.d.ts +125 -0
  14. package/dist/src/db/queries/subgraph-operations.d.ts +125 -0
  15. package/dist/src/db/queries/subgraphs.d.ts +125 -0
  16. package/dist/src/db/queries/sync-scope.d.ts +125 -0
  17. package/dist/src/db/queries/webhook-replays.d.ts +1431 -0
  18. package/dist/src/db/queries/webhook-replays.js +156 -0
  19. package/dist/src/db/queries/webhook-replays.js.map +10 -0
  20. package/dist/src/db/queries/webhooks.d.ts +125 -0
  21. package/dist/src/db/schema.d.ts +136 -2
  22. package/dist/src/index.d.ts +146 -2
  23. package/dist/src/index.js +15 -1
  24. package/dist/src/index.js.map +4 -4
  25. package/dist/src/insights/index.d.ts +142 -3
  26. package/dist/src/insights/index.js +40 -3
  27. package/dist/src/insights/index.js.map +3 -3
  28. package/dist/src/node/local-client.d.ts +125 -0
  29. package/dist/src/queue/listener.js +11 -1
  30. package/dist/src/queue/listener.js.map +3 -3
  31. package/dist/src/schemas/index.js +5 -1
  32. package/dist/src/schemas/index.js.map +3 -3
  33. package/dist/src/schemas/webhooks.d.ts +45 -1
  34. package/dist/src/schemas/webhooks.js +6 -1
  35. package/dist/src/schemas/webhooks.js.map +3 -3
  36. package/migrations/0177_oauth_mcp.ts +122 -0
  37. package/migrations/0178_insight_snooze.ts +22 -0
  38. package/migrations/0179_webhook_replays.ts +56 -0
  39. package/migrations/0180_push.ts +54 -0
  40. package/migrations/0181_push_sent.ts +30 -0
  41. package/package.json +5 -1
@@ -0,0 +1,122 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onControlPlane } from "../src/db/migration-role.ts";
3
+
4
+ /**
5
+ * OAuth for the hosted MCP endpoint (`<api>/mcp`). An app registers itself
6
+ * (`oauth_clients`, public clients only), sends the person to authorize
7
+ * (`oauth_requests`, answered on the web consent page), swaps the one-time
8
+ * code (`oauth_codes`) for tokens, and holds a grant (`oauth_grants`) with one
9
+ * live refresh token and short-lived access tokens (`oauth_tokens`). A rotated
10
+ * refresh token is remembered in `oauth_refresh_retired` so a replay revokes
11
+ * the grant. Every code and token is stored as its SHA-256 hash only.
12
+ */
13
+ export async function up(db: Kysely<unknown>): Promise<void> {
14
+ await onControlPlane(async () => {
15
+ await sql`
16
+ CREATE TABLE oauth_clients (
17
+ id text PRIMARY KEY,
18
+ name text NOT NULL,
19
+ redirect_uris text[] NOT NULL,
20
+ created_ip text,
21
+ created_at timestamptz NOT NULL DEFAULT now(),
22
+ last_used_at timestamptz
23
+ )
24
+ `.execute(db);
25
+ await sql`
26
+ CREATE INDEX oauth_clients_unused
27
+ ON oauth_clients (created_at) WHERE last_used_at IS NULL
28
+ `.execute(db);
29
+
30
+ await sql`
31
+ CREATE TABLE oauth_requests (
32
+ id text PRIMARY KEY,
33
+ client_id text NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
34
+ redirect_uri text NOT NULL,
35
+ state text,
36
+ code_challenge text NOT NULL,
37
+ resource text NOT NULL,
38
+ status text NOT NULL DEFAULT 'pending'
39
+ CHECK (status IN ('pending', 'approved', 'denied')),
40
+ account_id uuid REFERENCES accounts(id) ON DELETE CASCADE,
41
+ created_at timestamptz NOT NULL DEFAULT now(),
42
+ expires_at timestamptz NOT NULL,
43
+ answered_at timestamptz
44
+ )
45
+ `.execute(db);
46
+ await sql`
47
+ CREATE INDEX oauth_requests_expires ON oauth_requests (expires_at)
48
+ `.execute(db);
49
+
50
+ await sql`
51
+ CREATE TABLE oauth_grants (
52
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
53
+ account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
54
+ client_id text NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
55
+ scope text NOT NULL DEFAULT 'mcp',
56
+ resource text NOT NULL,
57
+ created_at timestamptz NOT NULL DEFAULT now(),
58
+ last_used_at timestamptz,
59
+ revoked_at timestamptz
60
+ )
61
+ `.execute(db);
62
+ await sql`
63
+ CREATE INDEX oauth_grants_account ON oauth_grants (account_id)
64
+ `.execute(db);
65
+
66
+ await sql`
67
+ CREATE TABLE oauth_codes (
68
+ code_hash text PRIMARY KEY,
69
+ client_id text NOT NULL REFERENCES oauth_clients(id) ON DELETE CASCADE,
70
+ account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
71
+ redirect_uri text NOT NULL,
72
+ code_challenge text NOT NULL,
73
+ resource text NOT NULL,
74
+ grant_id uuid REFERENCES oauth_grants(id) ON DELETE SET NULL,
75
+ created_at timestamptz NOT NULL DEFAULT now(),
76
+ expires_at timestamptz NOT NULL,
77
+ used_at timestamptz
78
+ )
79
+ `.execute(db);
80
+ await sql`
81
+ CREATE INDEX oauth_codes_expires ON oauth_codes (expires_at)
82
+ `.execute(db);
83
+
84
+ await sql`
85
+ CREATE TABLE oauth_tokens (
86
+ token_hash text PRIMARY KEY,
87
+ grant_id uuid NOT NULL REFERENCES oauth_grants(id) ON DELETE CASCADE,
88
+ kind text NOT NULL CHECK (kind IN ('access', 'refresh')),
89
+ created_at timestamptz NOT NULL DEFAULT now(),
90
+ expires_at timestamptz NOT NULL
91
+ )
92
+ `.execute(db);
93
+ await sql`
94
+ CREATE INDEX oauth_tokens_grant ON oauth_tokens (grant_id)
95
+ `.execute(db);
96
+ await sql`
97
+ CREATE INDEX oauth_tokens_expires ON oauth_tokens (expires_at)
98
+ `.execute(db);
99
+
100
+ await sql`
101
+ CREATE TABLE oauth_refresh_retired (
102
+ token_hash text PRIMARY KEY,
103
+ grant_id uuid NOT NULL REFERENCES oauth_grants(id) ON DELETE CASCADE,
104
+ retired_at timestamptz NOT NULL DEFAULT now()
105
+ )
106
+ `.execute(db);
107
+ await sql`
108
+ CREATE INDEX oauth_refresh_retired_at ON oauth_refresh_retired (retired_at)
109
+ `.execute(db);
110
+ });
111
+ }
112
+
113
+ export async function down(db: Kysely<unknown>): Promise<void> {
114
+ await onControlPlane(async () => {
115
+ await sql`DROP TABLE IF EXISTS oauth_refresh_retired`.execute(db);
116
+ await sql`DROP TABLE IF EXISTS oauth_tokens`.execute(db);
117
+ await sql`DROP TABLE IF EXISTS oauth_codes`.execute(db);
118
+ await sql`DROP TABLE IF EXISTS oauth_grants`.execute(db);
119
+ await sql`DROP TABLE IF EXISTS oauth_requests`.execute(db);
120
+ await sql`DROP TABLE IF EXISTS oauth_clients`.execute(db);
121
+ });
122
+ }
@@ -0,0 +1,22 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onControlPlane } from "../src/db/migration-role.ts";
3
+
4
+ /**
5
+ * Hides an open insight until this time ("Until tomorrow" on the phone).
6
+ * Survives re-fires, like `dismissed_at`.
7
+ */
8
+ export async function up(db: Kysely<unknown>): Promise<void> {
9
+ await onControlPlane(async () => {
10
+ await sql`
11
+ ALTER TABLE insights ADD COLUMN IF NOT EXISTS snoozed_until timestamptz
12
+ `.execute(db);
13
+ });
14
+ }
15
+
16
+ export async function down(db: Kysely<unknown>): Promise<void> {
17
+ await onControlPlane(async () => {
18
+ await sql`ALTER TABLE insights DROP COLUMN IF EXISTS snoozed_until`.execute(
19
+ db,
20
+ );
21
+ });
22
+ }
@@ -0,0 +1,56 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onControlPlane } from "../src/db/migration-role.ts";
3
+
4
+ /**
5
+ * One stored record per webhook replay, so a replay can be watched (queued,
6
+ * delivering N of M, done) and what gave up can be requeued. The partial
7
+ * unique index lets the database, not process memory, enforce "one replay
8
+ * scanning per webhook at a time".
9
+ *
10
+ * `webhook_outbox.replay_id` links outbox rows to their replay. It is a
11
+ * nullable column with no default (instant) and deliberately has NO index:
12
+ * building one on the large, hot outbox would block delivery inserts on every
13
+ * hosted stack. Progress counts use the existing partial index on pending rows.
14
+ */
15
+ export async function up(db: Kysely<unknown>): Promise<void> {
16
+ await onControlPlane(async () => {
17
+ await sql`
18
+ CREATE TABLE webhook_replays (
19
+ id text PRIMARY KEY,
20
+ webhook_id uuid NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
21
+ from_block bigint NOT NULL,
22
+ to_block bigint NOT NULL,
23
+ status text NOT NULL CHECK (status IN ('scanning','running','done','failed')),
24
+ scanned_count integer NOT NULL DEFAULT 0,
25
+ enqueued_count integer NOT NULL DEFAULT 0,
26
+ total integer,
27
+ delivered integer,
28
+ gave_up integer,
29
+ error text,
30
+ started_at timestamptz NOT NULL DEFAULT now(),
31
+ finished_at timestamptz
32
+ )
33
+ `.execute(db);
34
+ await sql`
35
+ CREATE UNIQUE INDEX webhook_replays_one_scanning
36
+ ON webhook_replays (webhook_id)
37
+ WHERE status = 'scanning'
38
+ `.execute(db);
39
+ await sql`
40
+ CREATE INDEX webhook_replays_webhook_started
41
+ ON webhook_replays (webhook_id, started_at DESC)
42
+ `.execute(db);
43
+ await sql`
44
+ ALTER TABLE webhook_outbox ADD COLUMN IF NOT EXISTS replay_id text
45
+ `.execute(db);
46
+ });
47
+ }
48
+
49
+ export async function down(db: Kysely<unknown>): Promise<void> {
50
+ await onControlPlane(async () => {
51
+ await sql`ALTER TABLE webhook_outbox DROP COLUMN IF EXISTS replay_id`.execute(
52
+ db,
53
+ );
54
+ await sql`DROP TABLE IF EXISTS webhook_replays`.execute(db);
55
+ });
56
+ }
@@ -0,0 +1,54 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onControlPlane } from "../src/db/migration-role.ts";
3
+
4
+ /**
5
+ * Web push for the installed phone app.
6
+ *
7
+ * `push_subscriptions`: one row per browser/device subscription. `endpoint` is
8
+ * unique, so the same browser re-subscribing (or signing in as another
9
+ * account) moves its row instead of duplicating it. `failed_count` counts
10
+ * delivery errors since the last success; 404/410 responses delete the row.
11
+ *
12
+ * `push_prefs`: one row per account, written on first change. A missing row
13
+ * means the column defaults, so most accounts never have one.
14
+ */
15
+ export async function up(db: Kysely<unknown>): Promise<void> {
16
+ await onControlPlane(async () => {
17
+ await sql`
18
+ CREATE TABLE push_subscriptions (
19
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
20
+ account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
21
+ endpoint text NOT NULL UNIQUE,
22
+ p256dh text NOT NULL,
23
+ auth text NOT NULL,
24
+ user_agent text,
25
+ created_at timestamptz NOT NULL DEFAULT now(),
26
+ last_success_at timestamptz,
27
+ failed_count integer NOT NULL DEFAULT 0
28
+ )
29
+ `.execute(db);
30
+ await sql`
31
+ CREATE INDEX push_subscriptions_account
32
+ ON push_subscriptions (account_id)
33
+ `.execute(db);
34
+ await sql`
35
+ CREATE TABLE push_prefs (
36
+ account_id uuid PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
37
+ webhook_failing boolean NOT NULL DEFAULT true,
38
+ subgraph_stopped boolean NOT NULL DEFAULT true,
39
+ subgraph_not_serving boolean NOT NULL DEFAULT true,
40
+ replay_done boolean NOT NULL DEFAULT true,
41
+ spend_cap boolean NOT NULL DEFAULT false,
42
+ key_repeat boolean NOT NULL DEFAULT false,
43
+ updated_at timestamptz NOT NULL DEFAULT now()
44
+ )
45
+ `.execute(db);
46
+ });
47
+ }
48
+
49
+ export async function down(db: Kysely<unknown>): Promise<void> {
50
+ await onControlPlane(async () => {
51
+ await sql`DROP TABLE IF EXISTS push_prefs`.execute(db);
52
+ await sql`DROP TABLE IF EXISTS push_subscriptions`.execute(db);
53
+ });
54
+ }
@@ -0,0 +1,30 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onControlPlane } from "../src/db/migration-role.ts";
3
+
4
+ /**
5
+ * One row per push already sent, so a signal that is seen again never pushes
6
+ * twice. `key` encodes "once per what" (an outage, a stop, a finished
7
+ * replay). Rows are purged after 30 days.
8
+ */
9
+ export async function up(db: Kysely<unknown>): Promise<void> {
10
+ await onControlPlane(async () => {
11
+ await sql`
12
+ CREATE TABLE push_sent (
13
+ account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
14
+ key text NOT NULL,
15
+ kind text NOT NULL,
16
+ sent_at timestamptz NOT NULL DEFAULT now(),
17
+ PRIMARY KEY (account_id, key)
18
+ )
19
+ `.execute(db);
20
+ await sql`
21
+ CREATE INDEX push_sent_sent_at ON push_sent (sent_at)
22
+ `.execute(db);
23
+ });
24
+ }
25
+
26
+ export async function down(db: Kysely<unknown>): Promise<void> {
27
+ await onControlPlane(async () => {
28
+ await sql`DROP TABLE IF EXISTS push_sent`.execute(db);
29
+ });
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secondlayer/shared",
3
- "version": "13.11.0",
3
+ "version": "13.13.0",
4
4
  "description": "Internal building blocks shared across Secondlayer packages.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -85,6 +85,10 @@
85
85
  "types": "./dist/src/db/queries/webhooks.d.ts",
86
86
  "import": "./dist/src/db/queries/webhooks.js"
87
87
  },
88
+ "./db/queries/webhook-replays": {
89
+ "types": "./dist/src/db/queries/webhook-replays.d.ts",
90
+ "import": "./dist/src/db/queries/webhook-replays.js"
91
+ },
88
92
  "./crypto/standard-webhooks": {
89
93
  "types": "./dist/src/crypto/standard-webhooks.d.ts",
90
94
  "import": "./dist/src/crypto/standard-webhooks.js"