diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae4dd65..249b947 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ These mirror `.github/workflows/ci.yml` exactly, so a clean local run means a gr **`pnpm run typecheck` requires `pnpm run build` first.** Packages import each other through their published entry points (`@haverstack/core`, `@haverstack/sqlite-shared`, …), which resolve to `dist/*.d.ts`. Without a build, those imports fail with `TS2307: Cannot find module '@haverstack/core'`, and every type that came through them degrades to `unknown` — producing a cascade of unrelated-looking errors (`TS18046: 'err' is of type 'unknown'`) in code that is perfectly fine. CI encodes this: its `typecheck` job declares `needs: build`. If you see a wall of `unknown` errors, check whether the _first_ error in the list is a missing module, and build before doing anything else. -**`pnpm test` does not need a build.** Each package's `vitest.config.ts` aliases `@haverstack/*` to the other package's `src/index.ts`, so tests always run against current source rather than a stale `dist`. A new cross-package dependency needs a matching alias added to the dependent package's vitest config, or its tests will resolve to `dist` and behave inconsistently. +**`pnpm test` does not need a build.** Each package's `vitest.config.ts` aliases `@haverstack/*` specifiers to the other package's `src/` files, so tests always run against current source rather than a stale `dist`. A new cross-package dependency needs a matching alias added to the dependent package's vitest config, or its tests will resolve to `dist` and behave inconsistently. Subpath specifiers (`@haverstack/core/testing`, `@haverstack/core/did`, `@haverstack/core/wire`, `@haverstack/core/adapter`) each need their own alias entry pointing at the matching entry-point file, listed before the bare `@haverstack/core` entry so the more specific match wins. --- diff --git a/README.md b/README.md index 07077d9..2d8c2e5 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ On the app's side, connecting is the keypair plus a URL. `APIAdapter` performs t ```ts import { APIAdapter } from '@haverstack/adapter-api'; -import { didCredentialFromKeypair } from '@haverstack/core'; +import { didCredentialFromKeypair } from '@haverstack/core/wire'; const adapter = await APIAdapter.open({ url: 'https://stack.example.com', @@ -97,7 +97,8 @@ Planned: ## Quick start ```ts -import { Stack, generateDidKeypair, exportDidPrivateKeyJwk } from '@haverstack/core'; +import { Stack } from '@haverstack/core'; +import { generateDidKeypair, exportDidPrivateKeyJwk } from '@haverstack/core/did'; import { LocalAdapter } from '@haverstack/adapter-local'; import { writeFile } from 'node:fs/promises'; @@ -243,7 +244,7 @@ The adapter interface is split into `StackRecordAdapter` (structured records) an | `adapter-api` | full | Hosted/shared stacks via HTTP | | `adapter-json` | full | Portable JSON files _(planned)_ | -Use `combineAdapters({ record, blob })` from `@haverstack/core` to compose a record adapter with a different blob backend — for example, `NativeSQLiteRecordAdapter` with a future `S3BlobAdapter`. `adapter-local` wraps this pattern for the common case. +Use `combineAdapters({ record, blob })` from `@haverstack/core/adapter` to compose a record adapter with a different blob backend — for example, `NativeSQLiteRecordAdapter` with a future `S3BlobAdapter`. `adapter-local` wraps this pattern for the common case. --- @@ -277,7 +278,10 @@ docs/ packages/ core/ # @haverstack/core src/ - index.ts # Public exports + index.ts # Root public exports — Stack, data types, general-purpose utilities + did-entry.ts # ./did public exports — keygen, custody, signing (did:key) + wire-entry.ts # ./wire public exports — auth handshake, attachment-download policy + adapter-entry.ts # ./adapter public exports — the interfaces a storage adapter implements types.ts # All type definitions (StackRecordAdapter, StackBlobAdapter, StackAdapter, …) stack.ts # Stack class combine.ts # combineAdapters() — compose record + blob adapters @@ -285,7 +289,10 @@ packages/ id.ts # Crockford base-32 ID generation schema.ts # Schema hashing and type compatibility validate.ts # Content validation - testing.ts # MemoryAdapter test helper (@haverstack/core/testing) + did.ts # did:key implementation + auth.ts # Auth handshake implementation + attachment-download.ts # Attachment download content-type resolution + testing.ts # MemoryAdapter test helper — exported as @haverstack/core/testing tests/ adapter-local/ # @haverstack/adapter-local src/ diff --git a/docs/design/change-events.md b/docs/design/change-events.md index cea8112..28ac35f 100644 --- a/docs/design/change-events.md +++ b/docs/design/change-events.md @@ -333,7 +333,8 @@ An app that keeps a live list of notes from a hosted stack. Everything outside t ### Connecting ```ts -import { Stack, didCredentialFromKeypair } from '@haverstack/core'; +import { Stack } from '@haverstack/core'; +import { didCredentialFromKeypair } from '@haverstack/core/wire'; import { APIAdapter } from '@haverstack/adapter-api'; // The keypair was generated once and persisted by the app — the stack never diff --git a/docs/spec/adapters.md b/docs/spec/adapters.md index fd03168..142c150 100644 --- a/docs/spec/adapters.md +++ b/docs/spec/adapters.md @@ -46,10 +46,10 @@ Packages follow a naming convention that makes the adapter type discoverable: `adapter-local` is the batteries-included package for the common local case. It wraps `NativeSQLiteRecordAdapter` and `DiskBlobAdapter` and stores attachments in an `attachments/` subdirectory next to the database file. Bearer tokens, when used, live in a separate sibling file (`.tokens`, via `NativeTokenStore`) — never inside the portable stack database. -Use `combineAdapters()` from `@haverstack/core` when you want different backends for records and blobs — for example, native SQLite records with S3 blob storage: +Use `combineAdapters()` from `@haverstack/core/adapter` when you want different backends for records and blobs — for example, native SQLite records with S3 blob storage: ```ts -import { combineAdapters } from '@haverstack/core'; +import { combineAdapters } from '@haverstack/core/adapter'; import { NativeSQLiteRecordAdapter } from '@haverstack/record-adapter-sqlite'; import { S3BlobAdapter } from '@haverstack/blob-adapter-s3'; // hypothetical diff --git a/docs/spec/identity.md b/docs/spec/identity.md index 6380f85..4e5e809 100644 --- a/docs/spec/identity.md +++ b/docs/spec/identity.md @@ -14,7 +14,7 @@ One claim in the served topology falls outside that guarantee, and it is named r | `did:web` | a domain in DID clothing | optional, for those who _want_ domain identity | | `did:plc` | ATProto's rotation directory | optional, for a future ATProto bridge | -`@haverstack/core` generates and verifies `did:key` (Ed25519) via `generateDidKeypair()` / `verifyDidSignature()` / etc. (`did.ts`) using Web Crypto only — zero infrastructure, zero resolution, zero registry, no dependency. Other methods are valid `entityId` values but core doesn't mint or resolve them. +`@haverstack/core/did` generates and verifies `did:key` (Ed25519) via `generateDidKeypair()` / `verifyDidSignature()` / etc. (`did.ts`) using Web Crypto only — zero infrastructure, zero resolution, zero registry, no dependency. Other methods are valid `entityId` values but core doesn't mint or resolve them. **Key custody is not this library's job.** `generateDidKeypair()` returns a `privateKey`; nothing in `@haverstack/core` or any adapter stores it — only the public DID travels with stack data. Where the private key lives (OS keychain, encrypted file, hardware key) and how it's backed up is an app/UX concern. @@ -177,7 +177,7 @@ Token issuance is not an out-of-band secret handoff, and the handshake that repl 2. Client signs a domain-separated payload binding the server's origin, its DID and the nonce — `buildAuthChallengePayload()` builds it, `signAuthChallenge()` signs it — and sends the signature back: `POST /auth/token { did, nonce, signature }`. 3. Server verifies against the payload it builds itself (`verifyAuthChallenge()` — for `did:key` this requires no lookup at all; the public key is decoded from the DID string) and, on success, calls `StackTokenStore.createToken(did)` and returns the bearer token. -`@haverstack/core` supplies both halves of steps 2 and 3 and runs no server; a server implementation brings the endpoints, nonce storage and single-use enforcement. +`@haverstack/core/wire` supplies both halves of steps 2 and 3 and runs no server; a server implementation brings the endpoints, nonce storage and single-use enforcement. "Access granted to the holder of key X" is verifiable with no provider, no email loop, no OAuth. diff --git a/docs/spec/wire-format.md b/docs/spec/wire-format.md index a939bbf..88166a4 100644 --- a/docs/spec/wire-format.md +++ b/docs/spec/wire-format.md @@ -90,7 +90,7 @@ Both are unauthenticated — they are how a token is obtained. `signature` is ba **The token's `expiresAt` is advisory.** A client MAY renew ahead of it and MUST NOT depend on it being present — it is optional, so 401-driven renewal is the floor a client needs regardless, and `APIAdapter` uses only that today. A server should therefore read an aggressive expiry as costing a wasted round-trip per lifetime rather than as something clients will schedule around. -**What gets signed is not the nonce.** It is a domain-separated payload, built identically on both sides by `buildAuthChallengePayload()` in `@haverstack/core` — exported for exactly this reason, so a server and a client cannot each derive "the same string" and diverge on the first ambiguity: +**What gets signed is not the nonce.** It is a domain-separated payload, built identically on both sides by `buildAuthChallengePayload()` in `@haverstack/core/wire` — exported for exactly this reason, so a server and a client cannot each derive "the same string" and diverge on the first ambiguity: ``` haverstack-auth-v1\n\n\n @@ -105,7 +105,7 @@ Two rules follow, and a server that skips either has a conformance gap rather th - **A server MUST build the payload from its own configured public origin**, never from a request header. `Host` and `X-Forwarded-Host` are client-controlled, so deriving the origin from one lets a client choose which origin it signs for, which is the whole property being bought. - **A server MUST verify against the payload it builds itself.** Nothing signed or claimed by the client contributes to it beyond the `did` and `nonce` fields named above. -`@haverstack/core` provides both halves — `signAuthChallenge()` / `verifyAuthChallenge()` — and `didCredentialFromKeypair()` builds the `{ did, sign }` credential `APIAdapter` takes. The credential is a **signing callback, never a private key**: key custody stays with the app (see [Identity](./identity.md)), so a caller is free to back it with a hardware key or a keychain prompt. +`@haverstack/core/wire` provides both halves — `signAuthChallenge()` / `verifyAuthChallenge()` — and `didCredentialFromKeypair()` builds the `{ did, sign }` credential `APIAdapter` takes. The credential is a **signing callback, never a private key**: key custody stays with the app (see [Identity](./identity.md)), so a caller is free to back it with a hardware key or a keychain prompt. ### Auth errors @@ -424,7 +424,7 @@ GET /attachments/?contentType=image/png&filename=photo.png - **Everything else** is forced to `Content-Type: application/octet-stream` with `Content-Disposition: attachment` — forcing the content type alone is not sufficient, since disposition determines whether a browser treats the response as inline-renderable at all. - **`X-Content-Type-Options: nosniff` is sent on every attachment download response**, forced or not — without it, browsers may sniff an `application/octet-stream` body back into the dangerous type the forcing just removed. -`@haverstack/core` exports the canonical implementation of this resolution and policy — `resolveAttachmentDownloadContentType()`, `isSafeAttachmentContentType()`, `inferContentTypeFromFilename()`, and the `NOSNIFF_HEADER_NAME`/`NOSNIFF_HEADER_VALUE` constants — so server implementations share one safe-list rather than each re-deriving it. +`@haverstack/core/wire` exports the canonical implementation of this resolution and policy — `resolveAttachmentDownloadContentType()`, `isSafeAttachmentContentType()`, `inferContentTypeFromFilename()`, and the `NOSNIFF_HEADER_NAME`/`NOSNIFF_HEADER_VALUE` constants — so server implementations share one safe-list rather than each re-deriving it. The filename in `Content-Disposition` is taken from `?filename` if given, else the requester's own `_attachment@1` record (if one exists), falling back to the first record's filename otherwise. diff --git a/packages/adapter-api/src/index.ts b/packages/adapter-api/src/index.ts index 2de7d4f..dbd24d3 100644 --- a/packages/adapter-api/src/index.ts +++ b/packages/adapter-api/src/index.ts @@ -16,12 +16,7 @@ * patchContent()/deleteRecord()/etc.'s expectedVersion option. */ -import { - assertQueryCapabilities, - StackQueryError, - buildAuthChallengePayload, - base64urlEncode, -} from '@haverstack/core'; +import { StackQueryError } from '@haverstack/core'; import type { StackAdapter, StackRecord, @@ -33,12 +28,14 @@ import type { QueryResult, Association, Permission, - AdapterCapabilities, RecordId, FileId, EntityId, - DidCredential, } from '@haverstack/core'; +import { assertQueryCapabilities } from '@haverstack/core/adapter'; +import type { AdapterCapabilities } from '@haverstack/core/adapter'; +import { buildAuthChallengePayload, base64urlEncode } from '@haverstack/core/wire'; +import type { DidCredential } from '@haverstack/core/wire'; import type { WireRecord, WireQueryResponse, diff --git a/packages/adapter-api/tests/api.test.ts b/packages/adapter-api/tests/api.test.ts index 5f6f798..6235ff1 100644 --- a/packages/adapter-api/tests/api.test.ts +++ b/packages/adapter-api/tests/api.test.ts @@ -11,7 +11,7 @@ import { APIAdapterHandshakeError, APIAdapterReauthError, } from '../src/index.js'; -import { buildAuthChallengePayload } from '@haverstack/core'; +import { buildAuthChallengePayload } from '@haverstack/core/wire'; import { WIRE_PROTOCOL_VERSION } from '@haverstack/wire-types'; import type { StackRecord, StackType, RecordVersion, Association } from '@haverstack/core'; import { diff --git a/packages/adapter-api/tests/conformance.test.ts b/packages/adapter-api/tests/conformance.test.ts index 63bc205..7f2c483 100644 --- a/packages/adapter-api/tests/conformance.test.ts +++ b/packages/adapter-api/tests/conformance.test.ts @@ -47,12 +47,14 @@ import { StackSchemaDriftError, StackPayloadTooLargeError, StackTimeoutError, +} from '@haverstack/core'; +import { buildAuthChallengePayload, verifyAuthChallenge, base64urlDecode, - generateDidKeypair, didCredentialFromKeypair, -} from '@haverstack/core'; +} from '@haverstack/core/wire'; +import { generateDidKeypair } from '@haverstack/core/did'; const BASE_URL = 'https://stack.example.com'; diff --git a/packages/adapter-api/vitest.config.ts b/packages/adapter-api/vitest.config.ts index 3c6ef71..5d7ed9a 100644 --- a/packages/adapter-api/vitest.config.ts +++ b/packages/adapter-api/vitest.config.ts @@ -4,6 +4,9 @@ import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { + '@haverstack/core/did': resolve(__dirname, '../core/src/did-entry.ts'), + '@haverstack/core/wire': resolve(__dirname, '../core/src/wire-entry.ts'), + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), '@haverstack/wire-types': resolve(__dirname, '../wire-types/src/index.ts'), '@haverstack/conformance-fixtures': resolve( diff --git a/packages/adapter-local/src/index.ts b/packages/adapter-local/src/index.ts index 846c4f2..44b3f25 100644 --- a/packages/adapter-local/src/index.ts +++ b/packages/adapter-local/src/index.ts @@ -10,7 +10,7 @@ * For most local use cases this is the only package you need. * If you want a different blob backend (e.g. S3), import * NativeSQLiteRecordAdapter and DiskBlobAdapter separately and - * compose them with combineAdapters() from @haverstack/core. + * compose them with combineAdapters() from @haverstack/core/adapter. */ import { dirname, join } from 'path'; @@ -25,20 +25,18 @@ import type { QueryResult, Association, Permission, - AdapterCapabilities, RecordId, FileId, - BlobFileInfo, - TokenInfo, TokenSession, } from '@haverstack/core'; +import type { AdapterCapabilities, BlobFileInfo, StackBlobAdapter } from '@haverstack/core/adapter'; +import type { TokenInfo } from '@haverstack/core/wire'; import { NativeSQLiteRecordAdapter, NativeTokenStore, defaultTokenStorePath, } from '@haverstack/record-adapter-sqlite'; import { DiskBlobAdapter } from '@haverstack/blob-adapter-disk'; -import type { StackBlobAdapter } from '@haverstack/core'; export { NativeSQLiteRecordAdapter, @@ -50,7 +48,8 @@ export type { NativeRecordOpenOptions, NativeTokenStoreOptions, } from '@haverstack/record-adapter-sqlite'; -export type { TokenInfo, TokenSession } from '@haverstack/core'; +export type { TokenSession } from '@haverstack/core'; +export type { TokenInfo } from '@haverstack/core/wire'; export { DiskBlobAdapter } from '@haverstack/blob-adapter-disk'; // ------------------------------------------------------- @@ -373,4 +372,4 @@ export class LocalAdapter implements StackAdapter { } // Also export combineAdapters for users who want to compose their own adapters -export { combineAdapters } from '@haverstack/core'; +export { combineAdapters } from '@haverstack/core/adapter'; diff --git a/packages/adapter-local/vitest.config.ts b/packages/adapter-local/vitest.config.ts index c5b5548..095612e 100644 --- a/packages/adapter-local/vitest.config.ts +++ b/packages/adapter-local/vitest.config.ts @@ -4,6 +4,8 @@ import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { + '@haverstack/core/wire': resolve(__dirname, '../core/src/wire-entry.ts'), + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), '@haverstack/record-adapter-sqlite': resolve( __dirname, diff --git a/packages/blob-adapter-disk/src/index.ts b/packages/blob-adapter-disk/src/index.ts index 622574c..706455b 100644 --- a/packages/blob-adapter-disk/src/index.ts +++ b/packages/blob-adapter-disk/src/index.ts @@ -16,7 +16,8 @@ import { mkdirSync, existsSync } from 'fs'; import { readFile, writeFile, unlink, readdir, stat, rename } from 'fs/promises'; import { join } from 'path'; import { StackNotFoundError, StackQueryError } from '@haverstack/core'; -import type { StackBlobAdapter, BlobFileInfo, FileId } from '@haverstack/core'; +import type { FileId } from '@haverstack/core'; +import type { StackBlobAdapter, BlobFileInfo } from '@haverstack/core/adapter'; const SHA256_HEX_RE = /^[0-9a-f]{64}$/; diff --git a/packages/blob-adapter-disk/vitest.config.ts b/packages/blob-adapter-disk/vitest.config.ts index e0ec2fb..024ed05 100644 --- a/packages/blob-adapter-disk/vitest.config.ts +++ b/packages/blob-adapter-disk/vitest.config.ts @@ -4,6 +4,7 @@ import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), }, }, diff --git a/packages/core/README.md b/packages/core/README.md index 9d7346f..36bc87b 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -19,7 +19,8 @@ You'll also need a storage adapter: ## Quick start ```ts -import { Stack, generateDidKeypair, exportDidPrivateKeyJwk } from '@haverstack/core'; +import { Stack } from '@haverstack/core'; +import { generateDidKeypair, exportDidPrivateKeyJwk } from '@haverstack/core/did'; import { LocalAdapter } from '@haverstack/adapter-local'; import { writeFile } from 'node:fs/promises'; diff --git a/packages/core/package.json b/packages/core/package.json index d2155a7..3ff3a00 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -8,6 +8,18 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts" }, + "./did": { + "import": "./dist/did-entry.js", + "types": "./dist/did-entry.d.ts" + }, + "./wire": { + "import": "./dist/wire-entry.js", + "types": "./dist/wire-entry.d.ts" + }, + "./adapter": { + "import": "./dist/adapter-entry.js", + "types": "./dist/adapter-entry.d.ts" + }, "./testing": { "import": "./dist/testing.js", "types": "./dist/testing.d.ts" diff --git a/packages/core/src/adapter-entry.ts b/packages/core/src/adapter-entry.ts new file mode 100644 index 0000000..f5dfeaa --- /dev/null +++ b/packages/core/src/adapter-entry.ts @@ -0,0 +1,18 @@ +/** + * @haverstack/core/adapter + * ------------------------------------------------------- + * The interfaces a storage adapter implements — record storage, blob + * storage, and the capabilities an adapter declares. Audience: adapter + * implementers (see the in-repo adapters), not app or plugin code, which + * reads `stack.features` instead of these directly. + */ + +export type { + StackRecordAdapter, + StackBlobAdapter, + AdapterCapabilities, + BlobFileInfo, + ExpectedVersionOptions, +} from './types.js'; +export { combineAdapters } from './combine.js'; +export { assertQueryCapabilities } from './stack.js'; diff --git a/packages/core/src/did-entry.ts b/packages/core/src/did-entry.ts new file mode 100644 index 0000000..a6ef6af --- /dev/null +++ b/packages/core/src/did-entry.ts @@ -0,0 +1,19 @@ +/** + * @haverstack/core/did + * ------------------------------------------------------- + * Key generation, custody and signing for did:key — the mandatory-floor + * DID method (see docs/spec/identity.md). Audience: apps at first-run + * keygen and key custody, and protocol implementers verifying signatures. + */ + +export { InvalidDidError, generateDidKeypair, verifyDidSignature } from './did.js'; +export type { DidKeypair } from './did.js'; + +// No in-repo caller: haverstack/server#54 validates ENTITY_ID as a DID at +// server startup. +export { isValidDid } from './did.js'; + +// No in-repo caller: root README.md § Key custody documents +// exportDidPrivateKeyJwk() -> store -> importDidPrivateKeyJwk() -> +// signWithDid() as the key-custody journey an app follows. +export { signWithDid, exportDidPrivateKeyJwk, importDidPrivateKeyJwk } from './did.js'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1210a9c..3b82a32 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,14 @@ * ------------------------------------------------------- * Core library for Haverstack — portable personal data stack. * - * Exports the Stack class, all types, and utility functions. + * Exports the Stack class, the app/plugin-facing data types, and the + * general-purpose utilities every caller needs. Audience-specific surfaces + * live behind their own subpaths, which this root does not re-export: + * ./did — key generation, custody and signing (did:key) + * ./wire — the auth handshake and attachment-download policy shared + * by both sides of a client/server connection + * ./adapter — the interfaces a storage adapter implements + * ./testing — the in-memory MemoryAdapter for tests * Storage adapters are published as separate packages: * @haverstack/adapter-sqlite */ @@ -24,7 +31,6 @@ export { StackPayloadTooLargeError, StackTimeoutError, StackClosedError, - assertQueryCapabilities, } from './stack.js'; export type { StackErrorCode, @@ -38,10 +44,6 @@ export type { CollectAttachmentGarbageResult, } from './stack.js'; -// Permissions -export { checkAccess, groupRoleFromAssociations } from './access.js'; -export type { AccessMode, RecordResolver, GroupRole } from './access.js'; - // Types export type { RecordId, @@ -70,16 +72,9 @@ export type { DateRange, Migration, MigrationFn, - AdapterCapabilities, - StackFeatures, - StackRecordAdapter, - ExpectedVersionOptions, - StackBlobAdapter, - BlobFileInfo, StackAdapter, - StackTokenStore, + StackFeatures, TokenSession, - TokenInfo, EntityContent, AppContent, GroupContent, @@ -90,67 +85,12 @@ export type { export { SYSTEM_TYPES } from './types.js'; -// Adapter composition -export { combineAdapters } from './combine.js'; - // Utilities -export { - generateId, - crockford32Encode, - crockford32Decode, - isValidIdFormat, - idTimestamp, -} from './id.js'; -export { - hashSchema, - isCompatible, - diffSchemas, - parseTypeId, - buildTypeId, - baseIdOf, -} from './schema.js'; + +// No in-repo caller: client-minted IDs are the default per spec, and an app +// needing the ID before the write round-trips has to mint one itself. +export { generateId } from './id.js'; +export { hashSchema, isCompatible } from './schema.js'; export type { SchemaDriftViolation } from './schema.js'; -export { - validateContent, - validateReservedKeys, - isValid, - RESERVED_CONTENT_KEYS, -} from './validate.js'; export type { ValidationError } from './validate.js'; export { applyMergePatch } from './merge.js'; -export { - InvalidDidError, - generateDidKeypair, - didFromPublicKey, - parseDidKey, - isValidDidKey, - publicKeyFromDidKey, - isValidDid, - signWithDid, - verifyDidSignature, - exportDidPrivateKeyJwk, - importDidPrivateKeyJwk, -} from './did.js'; -export type { DidKeypair } from './did.js'; -export { - InvalidAuthChallengeError, - AUTH_PAYLOAD_LABEL, - authOriginFromUrl, - buildAuthChallengePayload, - signAuthChallenge, - verifyAuthChallenge, - didCredentialFromKeypair, - base64urlEncode, - base64urlDecode, -} from './auth.js'; -export type { AuthChallenge, DidCredential } from './auth.js'; -export { - isSafeAttachmentContentType, - inferContentTypeFromFilename, - resolveAttachmentDownloadContentType, - firstRecordedAttachment, - FORCED_CONTENT_TYPE, - NOSNIFF_HEADER_NAME, - NOSNIFF_HEADER_VALUE, -} from './attachment-download.js'; -export type { AttachmentDownloadContentType } from './attachment-download.js'; diff --git a/packages/core/src/wire-entry.ts b/packages/core/src/wire-entry.ts new file mode 100644 index 0000000..284be19 --- /dev/null +++ b/packages/core/src/wire-entry.ts @@ -0,0 +1,47 @@ +/** + * @haverstack/core/wire + * ------------------------------------------------------- + * The handshake and attachment-download policy shared by both sides of a + * client/server connection — building a payload identically on both ends is + * the whole point, so this is imported by clients (e.g. adapter-api) and + * servers alike. See docs/spec/wire-format.md. + */ + +export { + buildAuthChallengePayload, + signAuthChallenge, + verifyAuthChallenge, + didCredentialFromKeypair, + base64urlEncode, + base64urlDecode, +} from './auth.js'; +export type { AuthChallenge, DidCredential } from './auth.js'; + +// No in-repo caller: a server must derive the signing origin from its own +// configured public origin (haverstack/server#53) — this is the primitive +// it does that with. +export { authOriginFromUrl } from './auth.js'; + +// Thrown by authOriginFromUrl, buildAuthChallengePayload and +// base64urlDecode above — all public, so a caller needs this to +// distinguish malformed input from any other Error. +export { InvalidAuthChallengeError } from './auth.js'; + +export { + isSafeAttachmentContentType, + inferContentTypeFromFilename, + resolveAttachmentDownloadContentType, + NOSNIFF_HEADER_NAME, + NOSNIFF_HEADER_VALUE, +} from './attachment-download.js'; +export type { AttachmentDownloadContentType } from './attachment-download.js'; + +// No in-repo caller: resolveAttachmentDownloadContentType() takes +// storedMimeType as an input rather than selecting the record itself — a +// server must call this to apply the earliest-createdAt/lowest-id total +// order and pass the winner in. Contract, not internal. haverstack/server#37 +export { firstRecordedAttachment } from './attachment-download.js'; + +// Server-facing: bearer-token issuance and lookup, backed by its own file +// outside the portable stack database — not a slot on StackAdapter. +export type { StackTokenStore, TokenInfo } from './types.js'; diff --git a/packages/core/tests/auth.test.ts b/packages/core/tests/auth.test.ts index 0395265..a5f7f59 100644 --- a/packages/core/tests/auth.test.ts +++ b/packages/core/tests/auth.test.ts @@ -9,10 +9,9 @@ import { didCredentialFromKeypair, base64urlEncode, base64urlDecode, - generateDidKeypair, - verifyDidSignature, -} from '../src/index.js'; -import type { DidKeypair } from '../src/index.js'; +} from '../src/auth.js'; +import { generateDidKeypair, verifyDidSignature } from '../src/did.js'; +import type { DidKeypair } from '../src/did.js'; const ORIGIN = 'https://stack.example.com'; const NONCE = 'k7Qm2ZxRt9vLbNc4Hy8Wf3'; diff --git a/packages/record-adapter-sqlite/src/index.ts b/packages/record-adapter-sqlite/src/index.ts index c8290ee..1f9ccad 100644 --- a/packages/record-adapter-sqlite/src/index.ts +++ b/packages/record-adapter-sqlite/src/index.ts @@ -27,22 +27,15 @@ import { DatabaseSync } from './node-sqlite.js'; import { existsSync } from 'fs'; -import type { - StackRecordAdapter, - StackType, - TypeId, - RecordId, - FileId, - RecordVersion, -} from '@haverstack/core'; +import type { StackType, TypeId, RecordId, FileId, RecordVersion } from '@haverstack/core'; import type { StackRecord, StackQuery, QueryResult, Association, Permission, - AdapterCapabilities, } from '@haverstack/core'; +import type { StackRecordAdapter, AdapterCapabilities } from '@haverstack/core/adapter'; import { RECORD_SCHEMA_SQL, FTS5_SCHEMA_SQL, diff --git a/packages/record-adapter-sqlite/src/token-store.ts b/packages/record-adapter-sqlite/src/token-store.ts index cc44a95..1fd416a 100644 --- a/packages/record-adapter-sqlite/src/token-store.ts +++ b/packages/record-adapter-sqlite/src/token-store.ts @@ -12,7 +12,8 @@ * SqlExecutor interface. */ -import type { StackTokenStore, TokenInfo, TokenSession } from '@haverstack/core'; +import type { TokenSession } from '@haverstack/core'; +import type { StackTokenStore, TokenInfo } from '@haverstack/core/wire'; import { TOKENS_SCHEMA_SQL, PRAGMA_JOURNAL_MODE_WAL, diff --git a/packages/record-adapter-sqlite/vitest.config.ts b/packages/record-adapter-sqlite/vitest.config.ts index cd0e1e5..d8e33e4 100644 --- a/packages/record-adapter-sqlite/vitest.config.ts +++ b/packages/record-adapter-sqlite/vitest.config.ts @@ -4,6 +4,8 @@ import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { + '@haverstack/core/wire': resolve(__dirname, '../core/src/wire-entry.ts'), + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), '@haverstack/sqlite-shared': resolve(__dirname, '../sqlite-shared/src/index.ts'), }, diff --git a/packages/sqlite-shared/src/token-logic.ts b/packages/sqlite-shared/src/token-logic.ts index 11aa4cd..c6a29f4 100644 --- a/packages/sqlite-shared/src/token-logic.ts +++ b/packages/sqlite-shared/src/token-logic.ts @@ -6,7 +6,8 @@ */ import { createHash, randomBytes } from 'node:crypto'; -import type { TokenInfo, TokenSession } from '@haverstack/core'; +import type { TokenSession } from '@haverstack/core'; +import type { TokenInfo } from '@haverstack/core/wire'; import type { SqlExecutor } from './executor.js'; import { toMs, fromMs } from './mappers.js'; diff --git a/packages/sqlite-shared/vitest.config.ts b/packages/sqlite-shared/vitest.config.ts index e0ec2fb..5a41823 100644 --- a/packages/sqlite-shared/vitest.config.ts +++ b/packages/sqlite-shared/vitest.config.ts @@ -4,6 +4,7 @@ import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { + '@haverstack/core/wire': resolve(__dirname, '../core/src/wire-entry.ts'), '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), }, }, diff --git a/packages/wire-types/src/index.ts b/packages/wire-types/src/index.ts index e8e480f..27fbad7 100644 --- a/packages/wire-types/src/index.ts +++ b/packages/wire-types/src/index.ts @@ -7,8 +7,8 @@ import type { ValidationError, SchemaDriftViolation, StackErrorCode, - AdapterCapabilities, } from '@haverstack/core'; +import type { AdapterCapabilities } from '@haverstack/core/adapter'; import { StackError, StackValidationError, diff --git a/packages/wire-types/vitest.config.ts b/packages/wire-types/vitest.config.ts index e0ec2fb..024ed05 100644 --- a/packages/wire-types/vitest.config.ts +++ b/packages/wire-types/vitest.config.ts @@ -4,6 +4,7 @@ import { resolve } from 'path'; export default defineConfig({ resolve: { alias: { + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), }, },