@agentage/extension 0.3.0 → 0.3.1

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.
@@ -10,7 +10,7 @@ export declare const partAuth: (env: Env, id: string, log: Log, scopes: string[]
10
10
  bearer_methods_supported: string[];
11
11
  };
12
12
  checks: {
13
- auth: import("../health/health.js").HealthCheck;
13
+ auth: () => Promise<"ok" | "degraded" | "unavailable">;
14
14
  };
15
15
  }>;
16
16
  export declare const declaredScopes: (id: string, openapi: Record<string, unknown> | null, tools?: readonly {
@@ -30,16 +30,19 @@ const settingsOf = (env) => {
30
30
  const { origin, hostname } = new URL(parsed.data.AGENTAGE_ORIGIN);
31
31
  if (!covers(audience, hostname))
32
32
  throw new Error(`the host ${hostname} is not under the audience ${audience}`);
33
+ if (origin.startsWith('https:') && !issuer.startsWith('https:'))
34
+ throw new Error(`the issuer ${issuer} is not https under the origin ${origin}`);
33
35
  return { issuer, audience, origin };
34
36
  };
35
- // Keys and revoked list read once before the part serves; an issuer down at start is no stop.
37
+ // Read before serving, then every 30 s (keys only while none held); no stop on a dead issuer.
36
38
  export const partAuth = async (env, id, log, scopes) => {
37
39
  const { issuer, audience, origin } = settingsOf(env);
38
- const { keys, read } = remoteKeys(issuer);
40
+ const { keys, read, held } = remoteKeys(issuer);
39
41
  const revoked = createRevokedList(issuer, log);
40
42
  const unread = (err) => log.warn('key set not read', { reason: messageOf(err) });
41
- await Promise.all([read().catch(unread), revoked.refresh()]);
42
- setInterval(() => void revoked.refresh(), REVOKED_POLL_MS).unref();
43
+ const poll = () => Promise.all([held() || read().catch(unread), revoked.refresh()]);
44
+ await poll();
45
+ setInterval(() => void poll(), REVOKED_POLL_MS).unref();
43
46
  return {
44
47
  tokenCheck: createTokenCheck({ issuer, audience, id, origin, keys, revoked }),
45
48
  resource: {
@@ -48,7 +51,7 @@ export const partAuth = async (env, id, log, scopes) => {
48
51
  scopes_supported: scopes,
49
52
  bearer_methods_supported: ['header'],
50
53
  },
51
- checks: { auth: revoked.check },
54
+ checks: { auth: async () => (held() ? revoked.check() : 'degraded') },
52
55
  };
53
56
  };
54
57
  export const declaredScopes = (id, openapi, tools = []) => {
@@ -14,5 +14,6 @@ export interface TokenCheckDeps {
14
14
  export declare const remoteKeys: (issuer: string) => {
15
15
  keys: JWTVerifyGetKey<Uint8Array<ArrayBufferLike> | import("node:crypto").webcrypto.CryptoKey>;
16
16
  read: () => Promise<void>;
17
+ held: () => boolean;
17
18
  };
18
19
  export declare const createTokenCheck: (deps: TokenCheckDeps) => TokenCheck;
@@ -33,7 +33,8 @@ export const remoteKeys = (issuer) => {
33
33
  timeoutDuration: 3000,
34
34
  cacheMaxAge: Infinity,
35
35
  });
36
- return { keys: keysOrOutage(set), read: () => retryOnce(set.reload) };
36
+ const held = () => set.jwks() !== undefined;
37
+ return { keys: keysOrOutage(set), read: () => retryOnce(set.reload), held };
37
38
  };
38
39
  const claimsOf = async (authorization, deps) => {
39
40
  const [scheme = '', credential = '', ...rest] = authorization.split(' ');
@@ -13,7 +13,7 @@ const partsOf = (pkg, images, migrations) => {
13
13
  parts['store'] = migrations === undefined ? { kind: store } : { kind: store, migrations };
14
14
  return parts;
15
15
  };
16
- // From the manifest, the build, the images by digest and what the code serves; OpenAPI comes later.
16
+ // From the manifest, the build, the images by digest and what the code serves.
17
17
  export const recordOf = (pkg, build, images, extras = {}) => {
18
18
  const { id, commands } = pkg.agentage;
19
19
  const { commit, buildTime } = build;
@@ -5,8 +5,7 @@ import { fail, isFailure } from '../envelope/envelope.js';
5
5
  import {} from '../log/log.js';
6
6
  import { prefixed } from './tools.js';
7
7
  const text = (value) => [{ type: 'text', text: value }];
8
- // A failed tool is isError with the REST error shape; anything unexpected is a generic INTERNAL.
9
- // Outside a request (no agcid in scope) the error still carries a valid one, minted here.
8
+ // A failed tool: isError, the REST error shape, an agcid (minted outside a request); else INTERNAL.
10
9
  const errorResult = (err, log) => {
11
10
  const failure = isFailure(err) ? err : fail('INTERNAL', 'Internal error');
12
11
  if (failure.status >= 500)
@@ -5,8 +5,7 @@ import { migrate } from './migrations.js';
5
5
  import { createStore } from './store.js';
6
6
  // A migration may rewrite a table: minutes, not a request's seconds; the job itself has no retry.
7
7
  const MIGRATE_TIMEOUTS = { connectMs: 10_000, statementMs: 300_000 };
8
- // The api image's migrate entry (`node dist/store/migrate.js` in /app), run as a job before a rollout.
9
- // Until the manifest is read the id is unknown; every line after it carries the extension.
8
+ // The api image's migrate job (`node dist/store/migrate.js`); lines before the manifest read 'unknown'.
10
9
  let log = createLog({ extension: 'unknown', part: 'migrate' });
11
10
  try {
12
11
  const pkg = await readManifest(process.cwd());
@@ -12,7 +12,7 @@ const TEST_BUILD = { commit: '0'.repeat(40), buildTime: '2026-01-01T00:00:00Z' }
12
12
  const testPackage = (ctx, parts) => ({
13
13
  name: `@agentage/extension-${ctx.extension.id}`,
14
14
  version: ctx.extension.version,
15
- engines: { agentage: '^0.2.0' },
15
+ engines: { agentage: '^0.3.0' },
16
16
  agentage: { id: ctx.extension.id, parts },
17
17
  });
18
18
  const startOn = async (app) => {
@@ -9,11 +9,11 @@ const ISSUER_ENTRY = 'node_modules/@agentage/extension/dist/tool/up/dev-issuer.j
9
9
  // A local-only credential: the store has no published port, only the parts' network reaches it.
10
10
  const STORE_PASSWORD = 'agentage-local';
11
11
  const storeUrl = (id) => `postgres://${id}:${STORE_PASSWORD}@store:5432/${id}`;
12
- const TOKENS = { AGENTAGE_ISSUER: UP_ISSUER, AGENTAGE_AUDIENCE: DEV_AUDIENCE };
13
12
  // Only the ui under `up` names the dev issuer: its CSP then lets the harness ask it for a token.
14
13
  const partEnv = (pkg, part) => ({
15
14
  AGENTAGE_ORIGIN: `http://localhost:${GATEWAY_PORT}`,
16
- ...TOKENS,
15
+ AGENTAGE_ISSUER: UP_ISSUER,
16
+ AGENTAGE_AUDIENCE: DEV_AUDIENCE,
17
17
  AGENTAGE_RECORD_FILE: RECORD_TARGET,
18
18
  ...(hasPostgres(pkg) && part !== 'ui' ? { AGENTAGE_STORE_URL: storeUrl(pkg.agentage.id) } : {}),
19
19
  ...(part === 'ui' ? { AGENTAGE_DEV_ISSUER: DEV_ISSUER } : {}),
@@ -23,10 +23,7 @@ const storeService = (id) => ({
23
23
  image: STORE_IMAGE,
24
24
  environment: { POSTGRES_USER: id, POSTGRES_PASSWORD: STORE_PASSWORD, POSTGRES_DB: id },
25
25
  volumes: ['store:/var/lib/postgresql'],
26
- healthcheck: {
27
- test: ['CMD-SHELL', `pg_isready -U ${id} -d ${id}`],
28
- ...QUICK_CHECK,
29
- },
26
+ healthcheck: { test: ['CMD-SHELL', `pg_isready -U ${id} -d ${id}`], ...QUICK_CHECK },
30
27
  });
31
28
  const HEALTHY = { condition: 'service_healthy' };
32
29
  // api and mcp read the issuer's keys and revoked list before they serve.
@@ -34,30 +31,26 @@ const dependsOn = (pkg, part) => ({
34
31
  ...(hasPostgres(pkg) && part !== 'ui' ? { store: HEALTHY } : {}),
35
32
  ...(part === 'api' || part === 'mcp' ? { issuer: HEALTHY } : {}),
36
33
  });
37
- const partService = (input, part) => {
38
- const depends_on = dependsOn(input.pkg, part);
39
- return {
40
- image: input.tags[part],
41
- environment: partEnv(input.pkg, part),
42
- volumes: [`${input.recordFile}:${RECORD_TARGET}:ro`],
43
- ...(Object.keys(depends_on).length > 0 ? { depends_on } : {}),
44
- };
45
- };
34
+ const partService = (input, part) => ({
35
+ image: input.tags[part],
36
+ environment: partEnv(input.pkg, part),
37
+ volumes: [`${input.recordFile}:${RECORD_TARGET}:ro`],
38
+ depends_on: dependsOn(input.pkg, part),
39
+ });
46
40
  const fixedServices = (input, anyImage) => ({
47
41
  gateway: {
48
42
  image: TRAEFIK_IMAGE,
49
43
  command: TRAEFIK_COMMAND,
50
44
  ports: [`127.0.0.1:${GATEWAY_PORT}:80`],
51
45
  volumes: [`${input.gatewayFile}:/etc/traefik/dynamic.yml:ro`],
52
- healthcheck: {
53
- test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:8082/ping'],
54
- ...QUICK_CHECK,
55
- },
46
+ healthcheck: { test: ['CMD', 'wget', '-qO-', 'http://127.0.0.1:8082/ping'], ...QUICK_CHECK },
56
47
  },
57
48
  issuer: {
58
49
  image: anyImage,
59
50
  command: ['node', ISSUER_ENTRY],
60
51
  ports: [`127.0.0.1:${ISSUER_PORT}:${PART_PORT}`],
52
+ healthcheck: { start_period: '15s', start_interval: '1s' }, // the image's test, each second
53
+ init: true, // node as PID 1 ignores SIGTERM: without it every stop waits 10 s
61
54
  },
62
55
  });
63
56
  const migrateService = (input) => ({
@@ -81,9 +81,8 @@ const revokedList = ({ revoked }) => {
81
81
  const ORIGINS = ['localhost', '127.0.0.1'].map((host) => `http://${host}:${GATEWAY_PORT}`);
82
82
  const allowed = ({ settings }, req) => {
83
83
  const host = (req.headers.host ?? '').replace(/:[0-9]+$/, '');
84
- const origin = req.headers.origin;
85
84
  const hosts = ['localhost', '127.0.0.1', new URL(settings.issuer).hostname];
86
- return hosts.includes(host) && (origin === undefined || ORIGINS.includes(origin));
85
+ return hosts.includes(host) && [undefined, ...ORIGINS].includes(req.headers.origin);
87
86
  };
88
87
  const route = (issuer, req, res) => {
89
88
  const at = `${req.method ?? ''} ${(req.url ?? '/').split('?')[0] ?? ''}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentage/extension",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "The agentage extension kit: create(ctx) per part (pages, ui, api, mcp, worker, cli), the store, /health, discovery, the envelope, and the build tool with up and token",
5
5
  "type": "module",
6
6
  "license": "MIT",