Skip to content

Commit d3ccfc1

Browse files
committed
chore(awareness-service): init migration, workspace and env wiring
- Add the initial TypeORM migration creating all AaaS tables and indexes. - Register services/*/* in the pnpm workspace so the api and portal packages resolve. - Add AaaS environment variables to .env.example. - Type jsonb columns as any so TypeORM's deep-partial insert types accept full packet payloads. - Add the service README.
1 parent bee5fdb commit d3ccfc1

8 files changed

Lines changed: 353 additions & 4 deletions

File tree

.env.example

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,27 @@ FILE_MANAGER_JWT_SECRET="secret"
129129
CHARTER_JWT_SECRET="secret"
130130
PICTIQUE_JWT_SECRET="secret"
131131

132+
133+
# Awareness as a Service (AaaS)
134+
# Connection string for the AaaS Postgres database
135+
AWARENESS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/awareness"
136+
AWARENESS_API_PORT=4100
137+
# Public base URL of the AaaS API (used to build W3DS auth callbacks)
138+
AWARENESS_PUBLIC_URL="http://localhost:4100"
139+
# Shared secret evault-core must present on POST /ingest
140+
AWARENESS_INGEST_SECRET="replace-with-a-strong-secret"
141+
# Where evault-core forwards every awareness packet
142+
AWARENESS_SERVICE_URL="http://localhost:4100"
143+
# Comma-separated eNames allowed to act as AaaS portal admins
144+
AAAS_ADMIN_ENAMES=""
145+
# Secret used to sign AaaS portal session JWTs
146+
AAAS_JWT_SECRET="replace-with-a-strong-secret"
147+
# Webhook delivery tuning
148+
AWARENESS_MAX_ATTEMPTS=8
149+
AWARENESS_DELIVERY_POLL_MS=2000
150+
# Neo4j source for the one-time backfill (evault-core's graph)
151+
AWARENESS_NEO4J_URI="bolt://localhost:7687"
152+
AWARENESS_NEO4J_USER="neo4j"
153+
AWARENESS_NEO4J_PASSWORD="your-neo4j-password"
154+
# Portal -> API base URL
155+
PUBLIC_AWARENESS_API_URL="http://localhost:4100"

pnpm-lock.yaml

Lines changed: 101 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ packages:
55
- platforms/*/*
66
- infrastructure/*
77
- services/*
8+
- services/*/*
89
- notification-trigger
910
- tests/
1011
- docs/
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Awareness as a Service (AaaS)
2+
3+
AaaS is the single fanout point for MetaEnvelope awareness packets. It replaces
4+
evault-core's built-in webhook fanout: evault-core now makes one POST to
5+
`AWARENESS_SERVICE_URL/ingest` per change, and AaaS owns persistence, polling,
6+
subscription matching and retrying webhook delivery.
7+
8+
## Packages
9+
10+
- `api/` — Express + TypeORM (Postgres) service.
11+
- `portal/` — SvelteKit + Tailwind public portal.
12+
13+
## What it does
14+
15+
1. **Ingest**`POST /ingest` receives every awareness packet from evault-core
16+
(shared-secret auth) and persists it.
17+
2. **Poll**`GET /api/packets` lets approved consumers query packet history by
18+
ontology, eVault and time range, with cursor pagination.
19+
3. **Subscribe**`/api/subscriptions` registers webhook subscriptions filtered
20+
by ontology and eVault. Delivered payloads match the legacy evault-core
21+
webhook format exactly.
22+
4. **Deliver** — a background engine drains the delivery queue with exponential
23+
backoff; exhausted deliveries land in a dead-letter table.
24+
5. **Portal** — platforms log in with W3DS, apply for access, and admins
25+
(`AAAS_ADMIN_ENAMES`) approve them. Approved consumers get API keys.
26+
27+
## Setup
28+
29+
```sh
30+
# 1. Create the Postgres database referenced by AWARENESS_DATABASE_URL
31+
# 2. Run migrations
32+
pnpm --filter awareness-service-api build
33+
pnpm --filter awareness-service-api migration:run
34+
35+
# 3. One-time backfill from evault-core's Neo4j (same node)
36+
pnpm --filter awareness-service-api backfill
37+
38+
# 4. Start the API (also seeds catch-all subscriptions on launch)
39+
pnpm --filter awareness-service-api dev
40+
41+
# 5. Start the portal
42+
pnpm --filter awareness-portal dev
43+
```
44+
45+
Then set `AWARENESS_SERVICE_URL` and `AWARENESS_INGEST_SECRET` for evault-core
46+
so it forwards packets here.
47+
48+
## Backward compatibility
49+
50+
On launch AaaS seeds a catch-all subscription for every platform currently in
51+
the registry, so existing webhook receivers keep getting every packet at
52+
`<platform>/api/webhook` with no change. Consumers can later narrow to specific
53+
ontologies / eVaults.

services/awareness-service/api/src/database/entities/DeadLetter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class DeadLetter {
2929

3030
/** The exact body that failed to deliver. */
3131
@Column({ type: "jsonb" })
32-
payload!: Record<string, unknown>;
32+
payload!: any;
3333

3434
@Column({ type: "varchar" })
3535
targetUrl!: string;

services/awareness-service/api/src/database/entities/Packet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ export class Packet {
3333
@Column({ type: "varchar", nullable: true })
3434
w3id!: string | null;
3535

36+
// typed as `any` so TypeORM's deep-partial insert/upsert types accept it.
3637
@Column({ type: "jsonb", nullable: true })
37-
data!: Record<string, unknown> | null;
38+
data!: any;
3839

3940
@Column({ type: "varchar", default: "create" })
4041
operation!: PacketOperation;
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
/**
4+
* Initial schema for Awareness as a Service: awareness packets, consumers and
5+
* their access applications, API keys, webhook subscriptions, the delivery
6+
* queue and the dead-letter table.
7+
*/
8+
export class Init1715200000000 implements MigrationInterface {
9+
name = "Init1715200000000";
10+
11+
public async up(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query(`
13+
CREATE TABLE "packets" (
14+
"id" varchar NOT NULL,
15+
"ontology" varchar NOT NULL,
16+
"evaultPublicKey" varchar,
17+
"w3id" varchar,
18+
"data" jsonb,
19+
"operation" varchar NOT NULL DEFAULT 'create',
20+
"receivedAt" timestamptz NOT NULL DEFAULT now(),
21+
"createdAt" timestamptz NOT NULL DEFAULT now(),
22+
CONSTRAINT "PK_packets" PRIMARY KEY ("id")
23+
)
24+
`);
25+
await queryRunner.query(
26+
`CREATE INDEX "idx_packets_ontology" ON "packets" ("ontology")`,
27+
);
28+
await queryRunner.query(
29+
`CREATE INDEX "idx_packets_evault_pubkey" ON "packets" ("evaultPublicKey")`,
30+
);
31+
await queryRunner.query(
32+
`CREATE INDEX "idx_packets_w3id" ON "packets" ("w3id")`,
33+
);
34+
await queryRunner.query(
35+
`CREATE INDEX "idx_packets_received" ON "packets" ("receivedAt")`,
36+
);
37+
await queryRunner.query(
38+
`CREATE INDEX "idx_packets_ontology_received" ON "packets" ("ontology", "receivedAt")`,
39+
);
40+
await queryRunner.query(
41+
`CREATE INDEX "idx_packets_received_id" ON "packets" ("receivedAt", "id")`,
42+
);
43+
44+
await queryRunner.query(`
45+
CREATE TABLE "consumers" (
46+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
47+
"ename" varchar NOT NULL,
48+
"name" varchar,
49+
"contactEmail" varchar,
50+
"status" varchar NOT NULL DEFAULT 'pending',
51+
"webhookBaseUrl" varchar,
52+
"createdAt" timestamptz NOT NULL DEFAULT now(),
53+
"approvedAt" timestamptz,
54+
CONSTRAINT "PK_consumers" PRIMARY KEY ("id")
55+
)
56+
`);
57+
await queryRunner.query(
58+
`CREATE UNIQUE INDEX "idx_consumers_ename" ON "consumers" ("ename")`,
59+
);
60+
61+
await queryRunner.query(`
62+
CREATE TABLE "access_applications" (
63+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
64+
"consumerId" uuid NOT NULL,
65+
"justification" text,
66+
"requestedOntologies" text array NOT NULL DEFAULT '{}',
67+
"status" varchar NOT NULL DEFAULT 'pending',
68+
"reviewedByEname" varchar,
69+
"reviewNote" text,
70+
"createdAt" timestamptz NOT NULL DEFAULT now(),
71+
"reviewedAt" timestamptz,
72+
CONSTRAINT "PK_access_applications" PRIMARY KEY ("id")
73+
)
74+
`);
75+
await queryRunner.query(
76+
`CREATE INDEX "idx_applications_consumer" ON "access_applications" ("consumerId")`,
77+
);
78+
79+
await queryRunner.query(`
80+
CREATE TABLE "api_keys" (
81+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
82+
"consumerId" uuid NOT NULL,
83+
"keyHash" varchar NOT NULL,
84+
"keyPrefix" varchar NOT NULL,
85+
"revoked" boolean NOT NULL DEFAULT false,
86+
"createdAt" timestamptz NOT NULL DEFAULT now(),
87+
"lastUsedAt" timestamptz,
88+
CONSTRAINT "PK_api_keys" PRIMARY KEY ("id")
89+
)
90+
`);
91+
await queryRunner.query(
92+
`CREATE INDEX "idx_api_keys_consumer" ON "api_keys" ("consumerId")`,
93+
);
94+
await queryRunner.query(
95+
`CREATE UNIQUE INDEX "idx_api_keys_hash" ON "api_keys" ("keyHash")`,
96+
);
97+
98+
await queryRunner.query(`
99+
CREATE TABLE "subscriptions" (
100+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
101+
"consumerId" uuid NOT NULL,
102+
"targetUrl" varchar NOT NULL,
103+
"ontologyFilter" text array NOT NULL DEFAULT '{}',
104+
"evaultFilter" text array NOT NULL DEFAULT '{}',
105+
"isCatchAll" boolean NOT NULL DEFAULT false,
106+
"active" boolean NOT NULL DEFAULT true,
107+
"secret" varchar,
108+
"createdAt" timestamptz NOT NULL DEFAULT now(),
109+
CONSTRAINT "PK_subscriptions" PRIMARY KEY ("id")
110+
)
111+
`);
112+
await queryRunner.query(
113+
`CREATE INDEX "idx_subscriptions_consumer" ON "subscriptions" ("consumerId")`,
114+
);
115+
116+
await queryRunner.query(`
117+
CREATE TABLE "deliveries" (
118+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
119+
"subscriptionId" uuid NOT NULL,
120+
"packetId" varchar NOT NULL,
121+
"status" varchar NOT NULL DEFAULT 'pending',
122+
"attempts" integer NOT NULL DEFAULT 0,
123+
"nextAttemptAt" timestamptz NOT NULL DEFAULT now(),
124+
"lastError" text,
125+
"lastResponseStatus" integer,
126+
"createdAt" timestamptz NOT NULL DEFAULT now(),
127+
"deliveredAt" timestamptz,
128+
CONSTRAINT "PK_deliveries" PRIMARY KEY ("id"),
129+
CONSTRAINT "uq_delivery_subscription_packet" UNIQUE ("subscriptionId", "packetId")
130+
)
131+
`);
132+
await queryRunner.query(
133+
`CREATE INDEX "idx_deliveries_subscription" ON "deliveries" ("subscriptionId")`,
134+
);
135+
await queryRunner.query(
136+
`CREATE INDEX "idx_deliveries_next_attempt" ON "deliveries" ("nextAttemptAt")`,
137+
);
138+
139+
await queryRunner.query(`
140+
CREATE TABLE "dead_letters" (
141+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
142+
"deliveryId" uuid NOT NULL,
143+
"subscriptionId" uuid NOT NULL,
144+
"packetId" varchar NOT NULL,
145+
"consumerId" uuid NOT NULL,
146+
"payload" jsonb NOT NULL,
147+
"targetUrl" varchar NOT NULL,
148+
"totalAttempts" integer NOT NULL,
149+
"lastError" text,
150+
"lastResponseStatus" integer,
151+
"resolved" boolean NOT NULL DEFAULT false,
152+
"createdAt" timestamptz NOT NULL DEFAULT now(),
153+
CONSTRAINT "PK_dead_letters" PRIMARY KEY ("id")
154+
)
155+
`);
156+
await queryRunner.query(
157+
`CREATE INDEX "idx_dead_letters_resolved" ON "dead_letters" ("resolved")`,
158+
);
159+
}
160+
161+
public async down(queryRunner: QueryRunner): Promise<void> {
162+
await queryRunner.query(`DROP TABLE "dead_letters"`);
163+
await queryRunner.query(`DROP TABLE "deliveries"`);
164+
await queryRunner.query(`DROP TABLE "subscriptions"`);
165+
await queryRunner.query(`DROP TABLE "api_keys"`);
166+
await queryRunner.query(`DROP TABLE "access_applications"`);
167+
await queryRunner.query(`DROP TABLE "consumers"`);
168+
await queryRunner.query(`DROP TABLE "packets"`);
169+
}
170+
}

0 commit comments

Comments
 (0)