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
58 changes: 58 additions & 0 deletions .llm/runs/docs-660b-sweep--opus/worklog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Worklog — docs-660b how-to + reference + pillar sweep (Opus 4.8)

Issue #660 slice B. Proposals #7, #8, #9, #10, #11, #12, #15 from `.llm/runs/docs-audit-662--sonnet/report.md`.

## Grounding / evidence

- **PORT/config (#7).** `packages/config/src/domain/schemas/service-schema.ts:11` types `port: z.number()`; `netscript-config-schema.ts:142,144` expose `services`/`apps` records. Scaffold entrypoints read `parseInt(Deno.env.get('PORT') || '<port>')` (`packages/cli/src/kernel/assets/embedded.generated.ts`), and the scaffolder writes the same value into config (`adapters/service/scaffolder.ts:115 port: options.servicePort`). Claim verified: Aspire injects PORT at runtime; `services.<name>.port` (apps: `apps.<name>.port`) is the typed source of truth.
- **#11 queue APIs.** `packages/queue/factory/{create-typed-queue,create-queue,create-parallel-queue}.ts`; `QueueProvider` enum `ports/options.ts:14`; `enqueue`/`listen` on `ports/message-queue.ts`. All cited symbols exist.
- **#11 workers executor.** `createDefaultTaskExecutor` + `TaskRuntimeAdapterLike` in `packages/plugin-workers-core/src/executor/` (import path `@netscript/plugin-workers-core/executor`).
- **#12 better-auth route guard.** `createBetterAuthBackend` returns `AuthBackendPort` with `sessions.getSession({ request })` and `principalMapper.mapSessionToPrincipal` (`packages/auth-better-auth/src/better-auth-backend.ts`). `AuthnRequest` exported from `@netscript/service/auth` (`packages/service/src/auth/mod.ts:53`); `AuthSession.state`/`Principal.roles` per auth.md tables.

## Per-page changes

(appended as slices land)

### #7 — PORT / typed-config consistency (8 pages)
One shared sentence at the first `parseInt(Deno.env.get('PORT')...)` occurrence per page:
`how-to/expose-openapi-scalar.md`, `how-to/add-a-service.md`, `tutorials/storefront/02-catalog-service.md`,
`tutorials/live-dashboard/02-contract-to-service.md`, `services-sdk/services.md`, `explanation/contracts.md`,
`how-to/add-opentelemetry.md` — all use `services.<name>.port`; `how-to/customize-fresh-ui.md` uses
`apps.<name>.port` (Fresh app). Commit 1.

### #8 — reference/ai/index.md
Added a block-quote note: AI provider keys have no typed config surface (by design); raw
`Deno.env.get` is the supported path, secrets injected by the runtime env. Commit 2.

### #9 — reference/queue/index.md + reference/kv/index.md
"See it live" link blocks (how-to/queue-kv-cron, how-to/choose-a-queue-provider, data-persistence
concept) before the reference-overview footer. Commit 2.

### #10 — reference/contracts/index.md + reference/sdk/index.md
Contracts lede: contracts obviate manual `req.json()` validation (rejected before handler runs).
SDK lede: `defineServices()` wires clients + query factories + query utils in one call. Commit 2.

### #15 — durable-workflows/streams.md
One sentence reframing "no in-process subscribe()" as an intentional single HTTP/SSE surface shared
by browser and server consumers. Commit 2.

### #11 — three decision how-tos
Runnable end-to-end block (resulting file + full command sequence) per page at deploy-local-aspire
density: `discover-services.md` (declare ref → service generate → aspire start → typed client),
`choose-a-queue-provider.md` (same call site on Deno KV then Redis), `add-a-task-runtime-adapter.md`
(node task file + runner + `deno run` + expected output). Commit 3.

### #12 — identity-access/better-auth-plugins.md
Complete `routes/admin/index.tsx` handler gated on active session + admin role via
`backend.sessions.getSession` / `principalMapper.mapSessionToPrincipal`, fail-closed. Commit 4.

## Validation
- Grep gate (public-docs law) on all 18 touched files: 0 hits.
- `deno task verify` from docs/site: build 512 files; check:links 23456 links across **169 pages**
all resolve; check:caveats 27 markers across 22 pages all resolve. 169 ≈ expected ~169 — base not stale.

## Commits
1. PORT/typed-config sweep (#7) — 8 pages
2. reference showcase/links + streams (#8 #9 #10 #15) — 6 pages
3. end-to-end how-to blocks (#11) — 3 pages
4. better-auth route guard (#12) — 1 page
5 changes: 4 additions & 1 deletion docs/site/durable-workflows/streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ The producer side is two calls: freeze a typed schema, open a stream, write
entity state. The consumer side is an HTTP/SSE read of the same `:4437` stream
path — there is no in-process `subscribe()` handle, so a Fresh island (or any
SSE client) reads the durable log directly and materializes the latest value per
key.
key. This is deliberate: exposing one HTTP/SSE surface instead of an in-process
`subscribe()` means a browser island and a server-side consumer read the durable
log through the exact same path and wire format, with no separate in-VM
subscription API to keep in sync.

{{ comp.tabbedCode({ tabs: [
{
Expand Down
4 changes: 4 additions & 0 deletions docs/site/explanation/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ two artifacts.
}
] }) }}

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source
of truth is your `netscript.config.ts` `services.<name>.port` field, which the scaffold wires as the
fallback default — set the port there rather than editing this line.

The `users` service listens on **port 3001** and exposes two parallel transports from the same
contract: REST-shaped routes under `/api/v1/users/*` (driven by `oc.route({ method, path })`
and surfaced in OpenAPI) and a typed RPC channel under `/api/rpc/v1/...` that the derived client
Expand Down
4 changes: 4 additions & 0 deletions docs/site/how-to/add-a-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ await defineService(router, {
});
```

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source
of truth is your `netscript.config.ts` `services.<name>.port` field, which the scaffold wires as the
fallback default — set the port there rather than editing this line.

`defineService` stands up the Hono + oRPC runtime, mounting your router under both an
OpenAPI surface (`/api/v1/users/*`) and the RPC surface (`/api/rpc/v1/...`). The default
RPC mount point is `/api/rpc` and the OpenAPI mount point is `/api`; both are overridable
Expand Down
48 changes: 48 additions & 0 deletions docs/site/how-to/add-a-task-runtime-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,54 @@ if (!result.success) {
}
```

## Run it end to end

The adapter and its registration above, run against a real task file. Copy the two files, then run
the command sequence from the workspace root:

```js
// tasks/render-invoice.mjs — the external Node task the adapter spawns
const idx = process.argv.indexOf('--invoice');
const invoice = idx >= 0 ? process.argv[idx + 1] : 'unknown';
console.log(`rendered ${invoice}`);
```

```ts
// run-invoice.ts — register the adapter and execute one task
import { createDefaultTaskExecutor } from '@netscript/plugin-workers-core/executor';
import { nodeAdapter } from './node-adapter.ts';

const executor = createDefaultTaskExecutor({
customAdapters: { node: nodeAdapter },
defaults: { cwd: Deno.cwd(), timeout: 300_000 },
});

const result = await executor.execute(
{
id: 'render-invoice',
type: 'node',
entrypoint: './tasks/render-invoice.mjs',
args: ['--invoice', 'inv_123'],
},
{ correlationId: 'invoice.inv_123', onStdout: (line) => console.log(line) },
);

if (!result.success) throw new Error(result.error ?? 'task failed');
console.log('status', result.status, 'exit', result.exitCode);
```

```bash
# From the workspace root. --allow-run=node lets the adapter spawn the Node
# binary; --allow-read lets it read the task file. Node must be on PATH.
deno run --allow-run=node --allow-read run-invoice.ts
# rendered inv_123
# status completed exit 0
```

`supports(task)` matches `task.type === 'node'`, so the executor routes `render-invoice` to your
adapter, which spawns `node ./tasks/render-invoice.mjs --invoice inv_123`, streams stdout through
`onStdout`, and returns a `TaskResult` with `status: 'completed'` and `exitCode: 0`.

## Failure modes

- `supports(task)` returns `false`: the executor returns a failed `TaskResult` for unsupported
Expand Down
4 changes: 4 additions & 0 deletions docs/site/how-to/add-opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ app with the fluent builder and opts the RPC layer into trace context explicitly
}
] }) }}

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source
of truth is your `netscript.config.ts` `services.<name>.port` field, which the scaffold wires as the
fallback default — set the port there rather than editing this line.

{{ comp callout { type: "note", title: "traceparent is the join key" } }}
NetScript follows the W3C Trace Context standard. When an inbound request carries a
<code>traceparent</code> header, the service continues that trace instead of starting a new
Expand Down
43 changes: 43 additions & 0 deletions docs/site/how-to/choose-a-queue-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,49 @@ await queue.listen(async (message) => {
For CPU-bound work, prefer Web Workers over queue concurrency — see
[Tune the worker runtime](/how-to/tune-worker-runtime/).

## Prove it end to end — same call site, two backends

The pluggability claim is only convincing if you can watch one file run unchanged on two backends.
Write the producer/consumer against the factory (no `provider`), then run it first on the Deno KV
fallback locally and again on Redis under Aspire — the file never changes.

```ts
// jobs.ts — the resulting file: provider-neutral producer + consumer
import { createTypedQueue } from '@netscript/queue';
import { z } from 'zod';

const JobSchema = z.object({ id: z.string(), kind: z.string() });

// No provider → auto-discovery. Probe order: RabbitMQ → Redis → Deno KV.
export const jobs = createTypedQueue('jobs', JobSchema);

if (import.meta.main) {
await jobs.enqueue({ id: 'job-1', kind: 'reindex' });
await jobs.listen(async (message) => {
console.log('processed', message.id, message.kind);
});
}
```

```bash
# From the workspace root, in order:

# 1. Local run — no Aspire, no broker. Auto-discovery falls through to Deno KV.
# The scaffold's deno.json already sets unstable: ['raw-imports', 'kv'];
# add --unstable-kv to any ad-hoc run that touches the queue.
deno run --unstable-kv -A jobs.ts
# -> processed job-1 reindex (on the Deno KV fallback)

# 2. Provision the real backend, then run the SAME file untouched.
cd aspire && aspire start && cd .. # brings up Redis/Garnet
deno run --unstable-kv -A jobs.ts
# -> processed job-1 reindex (now auto-discovered onto Redis)
```

The call site — `createTypedQueue('jobs', JobSchema)`, `enqueue`, `listen` — is identical in both
runs; only the resolved backend changed. Pin a specific provider with `provider: QueueProvider.*`
(Step 2) when you want to take auto-discovery out of the loop.

## In-production pitfalls

{{ comp callout { type: "warning", title: "Choosing a provider — read before you ship" } }}
Expand Down
4 changes: 4 additions & 0 deletions docs/site/how-to/customize-fresh-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ and components:
}
] }) }}

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source
of truth is your `netscript.config.ts` `apps.<name>.port` field, which the scaffold wires as the
fallback default — set the port there rather than editing this line.

{{ comp callout { type: "note", title: "Two import roots, two jobs" } }}
<code>@netscript/fresh</code> is the <strong>runtime meta-framework</strong>: it
ships the app bootstrap (<code>/server</code>), route and page builders, and a typed
Expand Down
46 changes: 46 additions & 0 deletions docs/site/how-to/discover-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,52 @@ to point a client at a fixed URL with no orchestrator:
services__users__http__0=http://localhost:3001 deno task --cwd web dev
```

## End to end in one pass

The four steps above, run start to finish against the example `users` service. Copy the file, then
run the command sequence from the workspace root:

```ts
// web/src/clients/users.ts — the resulting file (contract in, typed client out)
import { createServiceClient, isDefinedError, safe } from '@netscript/sdk/client';
import { UsersContractV1 } from '@my-app/contracts';

// serviceName is the discovery key — resolves services__users__http__0 at call time.
export const usersClient = createServiceClient({
contract: UsersContractV1,
serviceName: 'users',
});

// A typed call with safe-style error handling — no hardcoded URL anywhere.
export async function firstUsers() {
const [error, result] = await safe(usersClient.list({ limit: 20 }));
if (error && isDefinedError(error)) {
throw new Error(`users.list failed: ${error.code}`);
}
return result.items;
}
```

```bash
# From the workspace root, in order:

# 1. Declare the dependency — add "users" to the caller's ServiceReferences
# array in aspire/appsettings.json (see Step 1 above).

# 2. Regenerate the Aspire helpers so the reference is wired into the AppHost.
netscript service generate

# 3. Bring the graph up — Aspire injects services__users__http__0 into the caller.
cd aspire && aspire start && cd ..

# 4. Run the caller; the typed client resolves the URL lazily on the first call.
deno task --cwd web dev
```

With `aspire start` up, `usersClient.list({ limit: 20 })` resolves
`services__users__http__0`, issues the oRPC call to `<resolved-url>/api/rpc/v1/users`, and returns
the contract's typed `items`. No registry, no `localhost:<port>`, no generated client file.

## Production pitfalls

{{ comp callout { type: "warning", title: "Footguns before you ship" } }}
Expand Down
4 changes: 4 additions & 0 deletions docs/site/how-to/expose-openapi-scalar.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ await defineService(router, {
});
```

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source
of truth is your `netscript.config.ts` `services.<name>.port` field, which the scaffold wires as the
fallback default — set the port there rather than editing this line.

This wires three routes onto the service:

{{ comp.apiTable({
Expand Down
76 changes: 76 additions & 0 deletions docs/site/identity-access/better-auth-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,82 @@ interactive-flow seam for the better-auth backend is tracked on the roadmap.
<!-- caveat: arch-debt:seamless-auth-roadmap -->
{{ /comp }}

## Protect a route with the resolved session

`createBetterAuthBackend` returns an `AuthBackendPort`, and the piece a page or API route needs is
`backend.sessions.getSession({ request })`: it validates the request's better-auth session cookie
through `auth.api.getSession` and returns the normalized `AuthSession` (or `undefined`). Map that
session to a `Principal` with `backend.principalMapper.mapSessionToPrincipal(session)` and you have
the scopes and roles to authorize on. The example below gates a Fresh route on an **active** session
and an `admin` role, failing closed on both.

```ts
// server/auth-backend.ts — one shared backend instance for the app
import { auth } from './better-auth.ts'; // your createNetscriptBetterAuth(...) instance
import { createBetterAuthBackend } from '@netscript/auth-better-auth';

export const backend = createBetterAuthBackend({
auth,
sessionTokenSecret: Deno.env.get('BETTER_AUTH_SECRET')!,
});
```

```tsx
// routes/admin/index.tsx — a page that only an authenticated admin can load
import { HttpError } from 'fresh';
import type { AuthnRequest } from '@netscript/service/auth';
import { define } from '@/utils/state.ts';
import { backend } from '../../server/auth-backend.ts';

// Adapt the Fresh Request into the AuthnRequest the backend port reads.
function toAuthnRequest(req: Request): AuthnRequest {
const url = new URL(req.url);
return {
header: (name) => req.headers.get(name) ?? undefined,
headers: () => req.headers,
cookie: (name) =>
req.headers.get('cookie')
?.split('; ')
.find((c) => c.startsWith(`${name}=`))
?.slice(name.length + 1),
method: req.method,
path: url.pathname,
};
}

export const handler = define.handlers(async (ctx) => {
// Resolve the session from the request's better-auth cookie.
const session = await backend.sessions.getSession({
request: toAuthnRequest(ctx.req),
});

// Fail closed: no active session → redirect to sign-in, never render anonymously.
if (!session || session.state !== 'active') {
return ctx.redirect('/api/v1/auth/signin');
}

// Map to a NetScript Principal and authorize on its roles.
const { principal } = backend.principalMapper.mapSessionToPrincipal(session);
if (!principal.roles.includes('admin')) {
throw new HttpError(403);
}

return { data: { subject: principal.subject, roles: principal.roles } };
});

export default define.page<typeof handler>(({ data }) => (
<main>
<h1>Admin dashboard</h1>
<p>Signed in as {data.subject} — roles: {data.roles.join(', ')}</p>
</main>
));
```

Because `getSession` returns a typed `AuthSession | undefined` and the state check is explicit, the
route cannot silently degrade to an anonymous render — the same fail-loud discipline the
[authentication](/identity-access/auth/) page describes for the backend port. A machine-to-machine
API route follows the identical shape: swap the redirect for `throw new HttpError(401)`.

## Where to go next

- {{ comp.xref({ key: "explain:auth-model" }) }} — how Principals, sessions, and backends fit
Expand Down
6 changes: 6 additions & 0 deletions docs/site/reference/ai/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ and providers through their config bags. Two adapters resolve an API key from th
> `Deno.env.get(...)` in your composition code. Reading a key from the environment requires
> `--allow-env` for the variable in question.

> **Provider keys have no typed config surface — by design.** Unlike service topology (typed
> through `@netscript/config`), AI provider credentials are **not** modelled in
> `netscript.config.ts`. Reading the key with raw `Deno.env.get(...)` (or letting the Anthropic /
> OpenRouter adapters resolve their env var) is the **supported** path — secrets stay out of the
> committed config and are injected by the runtime environment instead.

## Examples

### Compose a runtime
Expand Down
7 changes: 5 additions & 2 deletions docs/site/reference/contracts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ title: "@netscript/contracts"

Contract vocabulary shared across NetScript package and plugin boundaries: the oRPC base
contract, common error data, pagination schemas, result types, and schema helper factories.
This page is generated from the package's public surface with `deno doc` (US-2). For the full
index of packages and plugins return to the [reference overview](/reference/).
Because a contract's input schema is enforced at the transport boundary, a service handler never
hand-parses or validates `req.json()` — a request that doesn't match the schema is rejected before
your code runs, and the same contract types the client that calls it. This page is generated from
the package's public surface with `deno doc` (US-2). For the full index of packages and plugins
return to the [reference overview](/reference/).

The root entrypoint (`@netscript/contracts`) carries the core contract primitives. Three
sub-path exports add higher-level builders:
Expand Down
Loading
Loading