Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,15 @@ import { expressApiKey, ApiKeyStore } from "@hasna/contracts/auth";
import { createCloudPoolFromEnv } from "./generated/storage-kit"; // vendored kit

const APP = "todos";
const signingSecret = process.env.HASNA_TODOS_API_SIGNING_KEY!; // shared: HASNA_API_SIGNING_KEY
const signingCredential = process.env.HASNA_TODOS_API_SIGNING_KEY!; // shared: HASNA_API_SIGNING_KEY
const { client } = createCloudPoolFromEnv(APP); // RDS pool (Amendment A1)
const keys = new ApiKeyStore(client);
await keys.ensureSchema(); // idempotent: api_keys table

app.use(
expressApiKey({
app: APP,
signingSecret,
signingSecret: signingCredential,
isRevoked: keys.isRevoked, // per-request revocation check against RDS
requiredScopes: ["todos:read"], // optional per-mount scope gate
audit: (e) => log.info("api_auth", e), // per-request AUDIT hook (allow + deny)
Expand Down Expand Up @@ -309,6 +309,28 @@ defaults are applied; output aliases such as `EvidenceRef` describe parsed data.
tracked kit version, declared bins, and the `local | cloud` storage boundary.
See `CONTRACT.md` for the normative spec and `contracts repo-conformance` /
`runRepoConformance` for the self-check kit.
- `hasna.comms_event_envelope.v1`: fleet comms event envelope carried in
conversations message metadata — namespaced `<source>.<entity>.<action>` type,
severity (`info | notice | breaking | critical`), scope
(`fleet | package | machine`), `affected_packages`/`affected_machines`,
`action_required`, `ack_by`, and a mandatory `dedupe_key`.
`fleet.freeze`/`fleet.unfreeze` are pinned critical + fleet-scoped +
action-required. The one severity mapping table ships as
`COMMS_EVENT_TYPES`/`COMMS_SEVERITY_TAG_INFO`.
- `hasna.comms_channel_metadata.v1`: the object stored under a conversations
channel's `metadata.channel_schema` key — channel `class`
(`fleet | package | product | loop-lane | initiative | personal`), noise class
(`quiet | work | firehose`), initiative `owner` + `until` horizon, and an
optional archived-channel `successor` pointer.
- `hasna.comms_message_metadata.v1`: structured metadata for severity-tagged
posts. The message text starts with `[FREEZE]`/`[UNFREEZE]`/`[BREAKING]`/
`[CUTOVER]`/`[POLICY]`/`[RELEASE]` as its exact-case first token; the tag plus
the full event envelope ride in `--metadata`, never parsed from text.
Publishers, hooks, and loops validate with `validateCommsTaggedMessage`
(or `extractCommsSeverityTag` + `validateContract`) before emit/post. The
human-facing rules live in knowledge items `hasna-agent-comms-protocol` /
`hasna-agent-comms-envelope`; these schemas are the machine-validatable
source of truth.
- `hasna.app.v1`: canonical app identity for the distribution apps plan —
stable `appId` slug, `npmName`, `repoFolder`, `githubUrl`, `projectSlug`,
surfaces (`bins`, optional `mcp`/`http`), lifecycle
Expand Down
7 changes: 7 additions & 0 deletions examples/comms-channel-metadata.invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schema": "hasna.comms_channel_metadata.v1",
"id": "research-shadow-fleet",
"createdAt": "2026-07-06T10:00:00.000Z",
"class": "initiative",
"noise": "work"
}
9 changes: 9 additions & 0 deletions examples/comms-channel-metadata.valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"schema": "hasna.comms_channel_metadata.v1",
"id": "oss-cloud-runtime",
"createdAt": "2026-07-06T10:00:00.000Z",
"class": "initiative",
"noise": "work",
"owner": "chief",
"until": "gate:97610c99"
}
14 changes: 14 additions & 0 deletions examples/comms-event-envelope.invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"schema": "hasna.comms_event_envelope.v1",
"id": "evt_release_breaking_bad_scope",
"createdAt": "2026-07-06T10:00:00.000Z",
"type": "release.breaking",
"severity": "breaking",
"scope": "package",
"summary": "Package-scoped breaking event without target packages and with an ack deadline but no action_required",
"affected_packages": [],
"affected_machines": [],
"action_required": false,
"ack_by": "2026-07-07T10:00:00.000Z",
"dedupe_key": "release.breaking:@hasna/loops@0.4.0"
}
26 changes: 26 additions & 0 deletions examples/comms-event-envelope.valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"schema": "hasna.comms_event_envelope.v1",
"id": "evt_fleet_freeze_20260706",
"createdAt": "2026-07-06T10:00:00.000Z",
"type": "fleet.freeze",
"severity": "critical",
"scope": "fleet",
"summary": "Freeze publish, deploy, and shared config apply during the conversations storage cutover",
"source": {
"kind": "agent",
"id": "chief",
"name": "chief"
},
"affected_packages": [],
"affected_machines": ["spark01", "spark02", "apple01", "apple03", "apple06"],
"action_required": true,
"ack_by": "2026-07-06T12:00:00.000Z",
"dedupe_key": "fleet.freeze:2026-07-06:conversations-storage-cutover",
"resourceRefs": [
{
"kind": "incident",
"id": "inc_conversations_cutover_20260706"
}
],
"evidenceRefs": []
}
22 changes: 22 additions & 0 deletions examples/comms-message-metadata.invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"schema": "hasna.comms_message_metadata.v1",
"id": "msg_release_tag_on_freeze_event",
"createdAt": "2026-07-06T10:00:05.000Z",
"tag": "RELEASE",
"envelope": {
"schema": "hasna.comms_event_envelope.v1",
"id": "evt_fleet_freeze_20260706",
"createdAt": "2026-07-06T10:00:00.000Z",
"type": "fleet.freeze",
"severity": "critical",
"scope": "fleet",
"summary": "A fleet.freeze event mislabeled as a release post",
"affected_packages": [],
"affected_machines": [],
"action_required": true,
"ack_by": "2026-07-06T12:00:00.000Z",
"dedupe_key": "fleet.freeze:2026-07-06:conversations-storage-cutover",
"resourceRefs": [],
"evidenceRefs": []
}
}
27 changes: 27 additions & 0 deletions examples/comms-message-metadata.valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"schema": "hasna.comms_message_metadata.v1",
"id": "msg_announcements_freeze_20260706",
"createdAt": "2026-07-06T10:00:05.000Z",
"tag": "FREEZE",
"envelope": {
"schema": "hasna.comms_event_envelope.v1",
"id": "evt_fleet_freeze_20260706",
"createdAt": "2026-07-06T10:00:00.000Z",
"type": "fleet.freeze",
"severity": "critical",
"scope": "fleet",
"summary": "Freeze publish, deploy, and shared config apply during the conversations storage cutover",
"source": {
"kind": "agent",
"id": "chief",
"name": "chief"
},
"affected_packages": [],
"affected_machines": ["spark01", "spark02", "apple01", "apple03", "apple06"],
"action_required": true,
"ack_by": "2026-07-06T12:00:00.000Z",
"dedupe_key": "fleet.freeze:2026-07-06:conversations-storage-cutover",
"resourceRefs": [],
"evidenceRefs": []
}
}
8 changes: 5 additions & 3 deletions src/client/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ describe("appendQuery", () => {
});

describe("HasnaStorageClient CRUD mapping", () => {
const sampleApiKey = "hasna_sample_key";

function client(handler: Parameters<typeof makeFetch>[0]) {
const { fetchImpl, calls } = makeFetch(handler);
const transport = createHasnaHttpTransport({ name: "knowledge", baseUrl: "https://knowledge.hasna.xyz/v1", apiKey: "hasna_knowledge_secret", fetchImpl, retry: false });
const transport = createHasnaHttpTransport({ name: "knowledge", baseUrl: "https://knowledge.hasna.xyz/v1", apiKey: sampleApiKey, fetchImpl, retry: false });
return { store: createHasnaStorageClient("knowledge", transport), calls };
}

Expand Down Expand Up @@ -63,8 +65,8 @@ describe("HasnaStorageClient CRUD mapping", () => {
const c = calls[0]!;
expect(c.method).toBe("POST");
expect(c.headers["idempotency-key"]).toBeTruthy();
expect(c.headers["authorization"]).toBe("Bearer hasna_knowledge_secret");
expect(c.headers["x-api-key"]).toBe("hasna_knowledge_secret");
expect(c.headers["authorization"]).toBe(`Bearer ${sampleApiKey}`);
expect(c.headers["x-api-key"]).toBe(sampleApiKey);
expect(c.body).toEqual({ title: "t" });
});

Expand Down
16 changes: 8 additions & 8 deletions src/client/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
// HASNA_<NAME>_API_URL = https://<app>.hasna.xyz
// <NAME>_API_URL (alias)
// API key (bearer / x-api-key):
// HASNA_<NAME>_API_KEY = hasna_<app>_...
// HASNA_<NAME>_API_KEY -> value from the app-owned vault
// <NAME>_API_KEY (alias)
//
// DECISION: transport is `cloud-http` IFF the resolved mode is `cloud` AND an API
Expand Down Expand Up @@ -52,16 +52,16 @@ export interface ClientTransportEnvKeys {

/** Resolve the canonical client-flip env-key spec for an app. */
export function clientTransportEnvKeys(name: string): ClientTransportEnvKeys {
const token = envToken(name);
const envSegment = envToken(name);
return {
modeKeys: [
`HASNA_${token}_STORAGE_MODE`,
`HASNA_${token}_MODE`,
`${token}_STORAGE_MODE`,
`${token}_MODE`,
`HASNA_${envSegment}_STORAGE_MODE`,
`HASNA_${envSegment}_MODE`,
`${envSegment}_STORAGE_MODE`,
`${envSegment}_MODE`,
],
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`],
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`],
};
}

Expand Down
Loading
Loading