@agentproto/auth 1.1.1 → 1.2.0

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.
package/README.md CHANGED
@@ -137,12 +137,16 @@ are Zod-validated at the boundary rather than trusted.
137
137
 
138
138
  ## Token storage
139
139
 
140
- `token-store.ts` wraps the macOS `security` CLI and **guards the platform** —
141
- `readKeychainToken` / `writeKeychainToken` throw a clear error on non-macOS hosts
142
- rather than silently returning `undefined` (which would re-prompt every run).
143
- Swap this module for libsecret (Linux) / Credential Manager (Windows) to run
144
- elsewhere. `resolveAccount(account, server)` expands the `{server}` template in a
145
- `tokenStore.account` spec.
140
+ `token-store.ts` is the platform switch and **guards the platform** —
141
+ `readKeychainToken` / `writeKeychainToken` throw a clear error where no backend
142
+ exists rather than silently returning `undefined` (which would re-prompt every
143
+ run). macOS wraps the `security` CLI; Windows (recap D7) seals one
144
+ DPAPI-protected file per slot under `~/.agentproto/keychain-dpapi/` via .NET
145
+ `ProtectedData` at `CurrentUser` scope through PowerShell — per-user
146
+ encryption, zero npm dependencies, and nothing beyond stock Windows
147
+ (Get-StoredCredential is NOT required). Linux is still unimplemented;
148
+ libsecret would be the equivalent there. `resolveAccount(account, server)`
149
+ expands the `{server}` template in a `tokenStore.account` spec.
146
150
 
147
151
  ## Credential store
148
152
 
@@ -229,6 +233,47 @@ exposure and have `resolveMcpHeaderExposure(exposure, broker)` lay brokered
229
233
  headers onto the transport. `secrets` keeps **zero dependency** on `auth`
230
234
  through that structural seam. See `@agentproto/secrets`' README.
231
235
 
236
+ ## Sub-accounts
237
+
238
+ An *account* (a login or billing identity) can have *sub-accounts* — orgs,
239
+ workspaces, projects — each with its own quota. A profile pins one with the
240
+ optional `subaccount` field, so every pin is a distinct, truthful wallet:
241
+
242
+ ```jsonc
243
+ { "id": "acme-ws01", "endpoint": "<endpoint>", "method": "api-key",
244
+ "source": "<account-source>", "subaccount": { "kind": "org", "id": "org_01ABC", "name": "Ws01" } }
245
+ ```
246
+
247
+ `kind` is `[a-z][a-z0-9-]*`; `id` is opaque; `name` is a display label used in
248
+ usage-limit tags. The field is optional and the on-disk store format is
249
+ unchanged. CLI: `agentproto auth profile create <id> --subaccount <kind>:<id>`
250
+ and `agentproto auth subaccounts list <profile|account> [--create [--prefix]]`.
251
+
252
+ How a pin is *applied* belongs to a provider, never to this package or the
253
+ runtime. A provider implements `SubaccountProvider` and is added with
254
+ `registerSubaccountProvider`:
255
+
256
+ | Member | Purpose |
257
+ | --- | --- |
258
+ | `id`, `source?`, `endpoints?`, `kinds` | identity, and which accounts it owns (by `source` name, else by endpoint) |
259
+ | `list(account)` | discovery: `{ account, subaccounts: [{kind, id, name}] }` |
260
+ | `resolve(profile, { credential? })` | spawn time: `{ credential?, credentialEnvOverride?, env?, isolateDataHome? }` |
261
+ | `migrateLegacySource?(source)` | read-time migration of a pre-pin provider-encoded `source` into the pin |
262
+
263
+ A provider throws `SubaccountError` when a pin cannot be listed or resolved; the
264
+ runtime fails the spawn loudly rather than falling back to the account's default
265
+ scope. Providers ship with their adapter and the host registers them (the CLI
266
+ does so for the built-ins).
267
+
268
+ The registry is process-global and starts EMPTY: a host that uses
269
+ `@agentproto/auth` outside the CLI must call `registerSubaccountProvider` for
270
+ each provider it needs BEFORE creating, reading or spawning pinned profiles.
271
+ Until then, creating a profile with a legacy `opencode-console:<orgId>` source is
272
+ rejected, and a spawn on a pinned profile whose provider is not registered fails
273
+ with `auth_source_unresolved` (it never runs on the account's default scope). A
274
+ pin on a credential-backed profile is accepted at create time even when no
275
+ provider owns its endpoint yet; the clear error comes at spawn time.
276
+
232
277
  ## API surface
233
278
 
234
279
  | Export | Purpose |
@@ -245,6 +290,8 @@ through that structural seam. See `@agentproto/secrets`' README.
245
290
  | `KeychainStore` / `MemoryStore` / `FileStore` / `resolveStoreRef` | built-in backends + ref resolver |
246
291
  | `readKeychainToken` / `writeKeychainToken` / `resolveAccount` | Keychain helpers |
247
292
  | `guildeAuthProvider` / `BUILTIN_AUTH_PROVIDERS` | shipped builtins |
293
+ | `registerSubaccountProvider` / `findSubaccountProvider` / `listSubaccountProviders` | sub-account provider registry |
294
+ | `SubaccountProvider` / `SubaccountPin` / `SubaccountError` / `parseSubaccountPin` | sub-account contract |
248
295
 
249
296
  ## License
250
297
 
package/dist/index.d.ts CHANGED
@@ -431,19 +431,23 @@ declare class CeremonyRequiredError extends Error {
431
431
  }
432
432
 
433
433
  /**
434
- * Keychain helpers — read/write a token in the platform Keychain.
434
+ * Keychain helpers — read/write a token in the platform key store.
435
435
  *
436
- * Uses the macOS `security` CLI. On non-macOS hosts, callers should swap this
437
- * module for a platform-appropriate equivalent (libsecret on Linux, Credential
438
- * Manager on Windows).
436
+ * Platform switch:
437
+ * - macOS → the `security` CLI.
438
+ * - Windows → a DPAPI-protected file per slot (`win-token-store.ts`, .NET
439
+ * `ProtectedData` at `CurrentUser` scope via PowerShell; per-user
440
+ * encryption, no PowerShell `Get-StoredCredential` module needed).
441
+ * - Anything else (Linux) → still unsupported; libsecret would be the
442
+ * equivalent to bring next.
439
443
  */
440
444
  /** Substitute `{server}` template in a tokenStore account spec. */
441
445
  declare function resolveAccount(account: string | undefined, server: string): string;
442
- /** Read a token from the Keychain. Returns undefined if not found. */
446
+ /** Read a token from the platform key store. Returns undefined if not found. */
443
447
  declare function readKeychainToken(service: string, account: string): Promise<string | undefined>;
444
- /** Write a token to the Keychain (-U updates in place). */
448
+ /** Write a token to the platform key store (upsert). */
445
449
  declare function writeKeychainToken(service: string, account: string, token: string): Promise<void>;
446
- /** Remove a token from the Keychain. Returns true if an entry was deleted,
450
+ /** Remove a token from the platform key store. Returns true if an entry was deleted,
447
451
  * false when none existed (a delete of an absent entry is not an error —
448
452
  * the desired end state, "no credential at this slot", already holds). */
449
453
  declare function deleteKeychainToken(service: string, account: string): Promise<boolean>;
@@ -451,9 +455,10 @@ declare function deleteKeychainToken(service: string, account: string): Promise<
451
455
  /**
452
456
  * KeychainStore — `CredentialStore` backed by the platform Keychain.
453
457
  *
454
- * Wraps the low-level `token-store.ts` helpers (macOS `security` CLI today).
455
- * The Keychain only holds one opaque string per entry, so `kind`/`expiresAt`/
456
- * `metadata` are packed into a small JSON envelope that IS that string.
458
+ * Wraps the low-level `token-store.ts` helpers (macOS `security` CLI; a
459
+ * DPAPI-protected file per slot on Windows). The key store only holds one
460
+ * opaque string per entry, so `kind`/`expiresAt`/`metadata` are packed
461
+ * into a small JSON envelope that IS that string.
457
462
  *
458
463
  * Back-compat: an entry written before this store existed (or by any other
459
464
  * tool) is a bare token string, not an envelope. `read` falls back to
@@ -604,6 +609,15 @@ interface CostBudget {
604
609
  /** Which spend surface the window is summed over. See {@link CostBudgetScope}. */
605
610
  scope: CostBudgetScope;
606
611
  }
612
+ /** A pinned sub-account of the profile's account: `kind` is free-form (`org`,
613
+ * `workspace`, `project`, …) and only meaningful to the provider that owns the
614
+ * account (see `subaccounts.ts`). */
615
+ interface SubaccountPin {
616
+ kind: string;
617
+ id: string;
618
+ /** Human-readable name captured at discovery time; informational only. */
619
+ name?: string;
620
+ }
607
621
  /** A named, billing-endpoint-scoped credential reference. */
608
622
  interface AuthProfile {
609
623
  /** Stable id, unique across all profiles (the `profileRef` a session
@@ -630,6 +644,13 @@ interface AuthProfile {
630
644
  * fresh at spawn time instead. Mutually exclusive with
631
645
  * {@link credentialRef}. */
632
646
  source?: string;
647
+ /** Pins one SUB-ACCOUNT (org / workspace / project / team) of the account this
648
+ * profile authenticates as — the account being the {@link credentialRef}'d
649
+ * secret or the {@link source}. Each pin is its own wallet; how it is
650
+ * applied at spawn is the registered provider's business (`subaccounts.ts`).
651
+ * ABSENT ⇒ the account's default scope, byte-identical to a profile that
652
+ * predates this field. */
653
+ subaccount?: SubaccountPin;
633
654
  /** Optional human-readable name ("Jeremy Max", "work OpenRouter"). */
634
655
  label?: string;
635
656
  /** Whole-profile enable/disable. ABSENT (or `false`) ⇒ enabled (today's
@@ -660,6 +681,114 @@ interface AuthProfile {
660
681
  origin?: string;
661
682
  }
662
683
 
684
+ /**
685
+ * Sub-accounts: an ACCOUNT (a login / billing identity) can have SUB-ACCOUNTS
686
+ * (org, workspace, project, team) with their own quota and billing. A profile
687
+ * can pin one ({@link AuthProfile.subaccount}); each pin is a distinct,
688
+ * truthful wallet. HOW a pin is applied to a spawn — headers, an inline config
689
+ * block, a scoped token, an isolated data home — is the provider's business.
690
+ *
691
+ * This module is the generic seam: the pin type, the {@link SubaccountProvider}
692
+ * interface, and a process-wide registry. Nothing here (or in any core spawn
693
+ * path) names a vendor; providers live with their adapter and are registered by
694
+ * the host (the CLI registers the built-ins at start-up).
695
+ */
696
+
697
+ /** Conservative charset for a pin's `kind` — it ends up in CLI args and logs. */
698
+ declare const SUBACCOUNT_KIND_RE: RegExp;
699
+ /** `"org:org_01ABC"` → `{ kind: "org", id: "org_01ABC" }`; split on the FIRST
700
+ * colon so an id may itself contain colons. Returns undefined when malformed. */
701
+ declare function parseSubaccountPin(spec: string): SubaccountPin | undefined;
702
+ /** The parent account a provider lists sub-accounts of. */
703
+ interface SubaccountAccountRef {
704
+ /** Billing endpoint the account authenticates against (`opencode-go`, …). */
705
+ endpoint: string;
706
+ /** Source-backed account (the account name a provider owns), no stored secret. */
707
+ source?: string;
708
+ /** Credential-backed account: the resolved secret. Never logged or returned. */
709
+ credential?: string;
710
+ }
711
+ interface DiscoveredSubaccount {
712
+ kind: string;
713
+ id: string;
714
+ name: string;
715
+ }
716
+ interface SubaccountListing {
717
+ account: {
718
+ id: string;
719
+ label?: string;
720
+ };
721
+ subaccounts: DiscoveredSubaccount[];
722
+ }
723
+ /** What a provider hands the spawn path for one pinned profile. */
724
+ interface SubaccountResolution {
725
+ /** Credential to inject, when the provider resolves its own (a source-backed
726
+ * account's scoped token). Omitted ⇒ the profile's stored credential stands. */
727
+ credential?: string;
728
+ /** Env var the credential is set into INSTEAD of the endpoint's conventional
729
+ * key env (the conventional one is then scrubbed). */
730
+ credentialEnvOverride?: string;
731
+ /** Extra env set verbatim beside the credential. A value that is a JSON
732
+ * object is deep-merged over a same-key value the spawn already carries
733
+ * (e.g. a mode's inline config), never clobbering it. */
734
+ env?: Record<string, string>;
735
+ /** `false` opts out of the adapter's isolated, login-less data home for this
736
+ * spawn; omitted ⇒ whatever the adapter declares applies. */
737
+ isolateDataHome?: boolean;
738
+ }
739
+ /** Raised by a provider when a sub-account cannot be listed or resolved. The
740
+ * runtime maps it onto its own spawn-error codes; the message must never carry
741
+ * a secret. */
742
+ declare class SubaccountError extends Error {
743
+ constructor(message: string);
744
+ }
745
+ interface SubaccountProvider {
746
+ /** Registry key, unique per provider. */
747
+ id: string;
748
+ /** The `source` name that identifies this provider's source-backed accounts
749
+ * (`AuthProfile.source`). */
750
+ source?: string;
751
+ /** Endpoints whose credential-backed profiles this provider serves. */
752
+ endpoints?: readonly string[];
753
+ /** Sub-account kinds this provider understands. */
754
+ kinds: readonly string[];
755
+ /** Endpoint a profile created from a discovered sub-account lands on. */
756
+ defaultEndpoint?: string;
757
+ /** Default id prefix for profiles created from a discovery (`<prefix>-<name>`). */
758
+ profilePrefix?: string;
759
+ /** Discovery: the sub-accounts of `account`. */
760
+ list(account: SubaccountAccountRef): Promise<SubaccountListing>;
761
+ /** Spawn-time: how the pinned `profile` is applied. `credential` is the
762
+ * profile's stored credential when it has one. */
763
+ resolve(profile: AuthProfile & {
764
+ subaccount: SubaccountPin;
765
+ }, ctx: {
766
+ credential?: string;
767
+ }): Promise<SubaccountResolution>;
768
+ /** Migrate-on-read hook for a profile shape that predates generic pins: a
769
+ * legacy `source` string → the account `source` + the pin it encoded. */
770
+ migrateLegacySource?(source: string): SubaccountPin | undefined;
771
+ }
772
+ /** Register (or replace, by `id`) a provider. Idempotent. */
773
+ declare function registerSubaccountProvider(provider: SubaccountProvider): void;
774
+ /** Remove a provider; returns whether it was registered. Mainly for tests. */
775
+ declare function unregisterSubaccountProvider(id: string): boolean;
776
+ declare function listSubaccountProviders(): SubaccountProvider[];
777
+ declare function getSubaccountProvider(id: string): SubaccountProvider | undefined;
778
+ /** The provider owning an account: by `source` name first, else by `endpoint`
779
+ * for a credential-backed account. */
780
+ declare function findSubaccountProvider(account: {
781
+ source?: string;
782
+ endpoint?: string;
783
+ }): SubaccountProvider | undefined;
784
+ /**
785
+ * Migrate-on-read: a profile that carries a legacy provider-encoded `source`
786
+ * (and no `subaccount`) is returned with the generic shape — `source` reduced
787
+ * to the account name, the encoded pin lifted into `subaccount`. Any other
788
+ * profile is returned as-is. Pure over the registry; never writes.
789
+ */
790
+ declare function migrateLegacySubaccountProfile(profile: AuthProfile): AuthProfile;
791
+
663
792
  /**
664
793
  * Named auth-profile store — CRUD over `~/.agentproto/auth-profiles.json`
665
794
  * (mode 0600), generalizing `providers-store`'s single-key-per-provider file
@@ -687,6 +816,11 @@ declare const authProfilesFileSchema: z.ZodObject<{
687
816
  }>;
688
817
  credentialRef: z.ZodOptional<z.ZodString>;
689
818
  source: z.ZodOptional<z.ZodString>;
819
+ subaccount: z.ZodOptional<z.ZodObject<{
820
+ kind: z.ZodString;
821
+ id: z.ZodString;
822
+ name: z.ZodOptional<z.ZodString>;
823
+ }, z.core.$strip>>;
690
824
  label: z.ZodOptional<z.ZodString>;
691
825
  disabled: z.ZodOptional<z.ZodBoolean>;
692
826
  models: z.ZodOptional<z.ZodObject<{
@@ -711,6 +845,11 @@ declare const authProfilesFileSchema: z.ZodObject<{
711
845
  method: "oauth-bearer" | "api-key";
712
846
  credentialRef?: string | undefined;
713
847
  source?: string | undefined;
848
+ subaccount?: {
849
+ kind: string;
850
+ id: string;
851
+ name?: string | undefined;
852
+ } | undefined;
714
853
  label?: string | undefined;
715
854
  disabled?: boolean | undefined;
716
855
  models?: {
@@ -761,9 +900,9 @@ declare function removeAuthProfile(id: string): Promise<boolean>;
761
900
 
762
901
  /** Input to {@link createAuthProfile}. `credential` is the raw secret — it is
763
902
  * written to the store and NEVER returned. Exactly one of `credential` /
764
- * `source` must be given for an `oauth-bearer` profile; `api-key` always
765
- * requires `credential` (a source-backed profile only makes sense for a
766
- * self-refreshing subscription bearer). */
903
+ * `source` must be given for an `oauth-bearer` profile; `api-key` requires
904
+ * `credential`, except for a `source` owned by a registered sub-account
905
+ * provider (an account resolved at spawn, pinned to a `subaccount`). */
767
906
  interface CreateAuthProfileInput {
768
907
  /** Stable id, unique across all profiles. */
769
908
  id: string;
@@ -780,6 +919,9 @@ interface CreateAuthProfileInput {
780
919
  * resolved fresh at spawn time instead. Mutually exclusive with
781
920
  * `credential`. */
782
921
  source?: string;
922
+ /** Pin one sub-account (org / workspace / project) of the account — the
923
+ * `credential` or the `source`. See `subaccounts.ts`. */
924
+ subaccount?: SubaccountPin;
783
925
  /** Optional human-readable name. */
784
926
  label?: string;
785
927
  /** Optional explicit credential-store slot. Omitted ⇒ derived from
@@ -801,6 +943,8 @@ interface CreatedAuthProfile {
801
943
  credentialRef?: string;
802
944
  /** Set for a source-backed profile; absent for a credential-backed one. */
803
945
  source?: string;
946
+ /** The pinned sub-account, when the profile has one. */
947
+ subaccount?: SubaccountPin;
804
948
  label?: string;
805
949
  /** Provenance stamped at import time, when given. */
806
950
  origin?: string;
@@ -840,6 +984,7 @@ interface ValidatedCreateInput {
840
984
  method: AuthMethod;
841
985
  credential?: string;
842
986
  source?: string;
987
+ subaccount?: SubaccountPin;
843
988
  label?: string;
844
989
  credentialRef?: string;
845
990
  origin?: string;
@@ -1092,4 +1237,4 @@ declare const BUILTIN_AUTH_PROVIDERS: readonly AuthProviderHandle[];
1092
1237
  declare const SPEC_NAME = "agentauth";
1093
1238
  declare const SPEC_VERSION = "v1";
1094
1239
 
1095
- export { type AdapterAuthManifest, type AuthConfig, type AuthMethod, type AuthProfile, AuthProfileValidationError, type AuthProfilesFile, type AuthProviderDefinition, type AuthProviderFrontmatter, type AuthProviderHandle, type AuthProviderManifest, BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, type CostBudget, type CostBudgetScope, type CreateAuthProfileInput, type CreatedAuthProfile, CredentialBroker, type CredentialBrokerOptions, type CredentialIdentity, type CredentialStore, type DeletedAuthProfile, type DeviceCodeAuthConfig, type DiscoveredEndpoints, DiscoveryError, FLOW_ENGINES, FileStore, type FlowEngine, type FlowId, type FlowResult, type FlowRunOptions, type InstallConfig, KeychainStore, MemoryStore, type ModelCuration, type PATAuthConfig, type ProfileProvisionDeps, type RefreshAuthProfileModelsResult, type RunFlowOptions, SPEC_NAME, SPEC_VERSION, type ServiceAuthConfig, type StoreRef, type StoredCredential, type TokenStoreSpec, type UpdateAuthProfileInput, type ValidatedCreateInput, addAuthProfile, authConfigSchema, authProfilesPath, authProviderFrontmatterSchema, createAuthProfile, credentialIdentity, defineAuthProvider, deleteAuthProfile, deleteKeychainToken, deriveCredentialRef, discoverEndpoints, eligibleProfiles, fingerprintCredential, getAuthProfile, getAuthProvider, guildeAuthProvider, installConfigSchema, listAuthProfiles, listAuthProviderIds, listAuthProviders, loadAuthProfiles, methodsPresentable, parseAuthProviderManifest, parseAuthProviderManifestRaw, readKeychainToken, refreshAuthProfileModels, registerAuthProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, setAuthProfileEnabled, setAuthProfileModels, tokenStoreSpecSchema, updateAuthProfile, validateCreateInput, writeKeychainToken };
1240
+ export { type AdapterAuthManifest, type AuthConfig, type AuthMethod, type AuthProfile, AuthProfileValidationError, type AuthProfilesFile, type AuthProviderDefinition, type AuthProviderFrontmatter, type AuthProviderHandle, type AuthProviderManifest, BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, type CostBudget, type CostBudgetScope, type CreateAuthProfileInput, type CreatedAuthProfile, CredentialBroker, type CredentialBrokerOptions, type CredentialIdentity, type CredentialStore, type DeletedAuthProfile, type DeviceCodeAuthConfig, type DiscoveredEndpoints, type DiscoveredSubaccount, DiscoveryError, FLOW_ENGINES, FileStore, type FlowEngine, type FlowId, type FlowResult, type FlowRunOptions, type InstallConfig, KeychainStore, MemoryStore, type ModelCuration, type PATAuthConfig, type ProfileProvisionDeps, type RefreshAuthProfileModelsResult, type RunFlowOptions, SPEC_NAME, SPEC_VERSION, SUBACCOUNT_KIND_RE, type ServiceAuthConfig, type StoreRef, type StoredCredential, type SubaccountAccountRef, SubaccountError, type SubaccountListing, type SubaccountPin, type SubaccountProvider, type SubaccountResolution, type TokenStoreSpec, type UpdateAuthProfileInput, type ValidatedCreateInput, addAuthProfile, authConfigSchema, authProfilesPath, authProviderFrontmatterSchema, createAuthProfile, credentialIdentity, defineAuthProvider, deleteAuthProfile, deleteKeychainToken, deriveCredentialRef, discoverEndpoints, eligibleProfiles, findSubaccountProvider, fingerprintCredential, getAuthProfile, getAuthProvider, getSubaccountProvider, guildeAuthProvider, installConfigSchema, listAuthProfiles, listAuthProviderIds, listAuthProviders, listSubaccountProviders, loadAuthProfiles, methodsPresentable, migrateLegacySubaccountProfile, parseAuthProviderManifest, parseAuthProviderManifestRaw, parseSubaccountPin, readKeychainToken, refreshAuthProfileModels, registerAuthProvider, registerSubaccountProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, setAuthProfileEnabled, setAuthProfileModels, tokenStoreSpecSchema, unregisterSubaccountProvider, updateAuthProfile, validateCreateInput, writeKeychainToken };
package/dist/index.mjs CHANGED
@@ -5,9 +5,9 @@ import { createInterface } from 'readline';
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { createHash, createDecipheriv, randomBytes, createCipheriv } from 'crypto';
8
- import { readFile, mkdir, writeFile } from 'fs/promises';
9
- import { dirname, resolve, join } from 'path';
10
8
  import { homedir } from 'os';
9
+ import { dirname, resolve, join } from 'path';
10
+ import { readFile, mkdir, writeFile } from 'fs/promises';
11
11
 
12
12
  /**
13
13
  * @agentproto/auth v0.1.0-alpha
@@ -337,10 +337,72 @@ async function discoverEndpoints(apiBase, opts = {}) {
337
337
  }
338
338
  }
339
339
  var exec = promisify(execFile);
340
+ function dpapiDir(deps) {
341
+ return join(homedir(), ".agentproto", "keychain-dpapi");
342
+ }
343
+ var FILE_HASH = (service, account) => createHash("sha256").update(`${service}\0${account}`).digest("hex").slice(0, 24);
344
+ function dpapiFile(service, account, deps) {
345
+ const svc = service.replace(/[^\dA-Za-z._-]/g, "_");
346
+ const acct = account.replace(/[^\dA-Za-z._-]/g, "_");
347
+ return join(dpapiDir(), `${svc}__${acct}__${FILE_HASH(service, account)}.dpapi`);
348
+ }
349
+ async function runPowerShell(args, deps) {
350
+ return exec("powershell", args, { timeout: 15e3 });
351
+ }
352
+ var B64 = (s) => Buffer.from(s, "utf8").toString("base64");
353
+ var DECODE = `$d={param($s)[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s))};`;
354
+ function buildDpapiWriteCommand(service, account, token, deps) {
355
+ const file = dpapiFile(service, account);
356
+ const script = `${DECODE}$ErrorActionPreference='Stop';$p=&$d('${B64(file)}');$raw=[Convert]::FromBase64String('${B64(token)}');Add-Type -AssemblyName System.Security;[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($p))|Out-Null;[IO.File]::WriteAllBytes($p, [Security.Cryptography.ProtectedData]::Protect($raw,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser))`;
357
+ return { cmd: "powershell", args: ["-NoProfile", "-NonInteractive", "-Command", script] };
358
+ }
359
+ function buildDpapiReadCommand(service, account, deps) {
360
+ const file = dpapiFile(service, account);
361
+ const script = `${DECODE}$ErrorActionPreference='Stop';$p=&$d('${B64(file)}');Add-Type -AssemblyName System.Security;$enc=[IO.File]::ReadAllBytes($p);$raw=[Security.Cryptography.ProtectedData]::Unprotect($enc,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($raw))`;
362
+ return { cmd: "powershell", args: ["-NoProfile", "-NonInteractive", "-Command", script] };
363
+ }
364
+ function buildDpapiDeleteCommand(service, account, deps) {
365
+ const file = dpapiFile(service, account);
366
+ const script = `${DECODE}$p=&$d('${B64(file)}');if(Test-Path -LiteralPath $p){Remove-Item -LiteralPath $p -Force;'1'}else{'0'}`;
367
+ return { cmd: "powershell", args: ["-NoProfile", "-NonInteractive", "-Command", script] };
368
+ }
369
+ function errMsg(err) {
370
+ const detail = typeof err === "object" && err !== null && "message" in err ? String(err.message) : String(err);
371
+ return detail.replace(/^Command failed: powershell.*?\n/, "").trim();
372
+ }
373
+ async function writeDpapiToken(service, account, token, deps) {
374
+ const { args } = buildDpapiWriteCommand(service, account, token);
375
+ try {
376
+ await runPowerShell(args, deps);
377
+ } catch (err) {
378
+ throw new Error(`agentproto auth: cannot write the Windows DPAPI credential store: ${errMsg(err)}`);
379
+ }
380
+ }
381
+ async function readDpapiToken(service, account, deps) {
382
+ const { args } = buildDpapiReadCommand(service, account);
383
+ try {
384
+ const { stdout } = await runPowerShell(args, deps);
385
+ return stdout.trimEnd() || void 0;
386
+ } catch {
387
+ return void 0;
388
+ }
389
+ }
390
+ async function deleteDpapiToken(service, account, deps) {
391
+ const { args } = buildDpapiDeleteCommand(service, account);
392
+ try {
393
+ const { stdout } = await runPowerShell(args, deps);
394
+ return stdout.trim() === "1";
395
+ } catch {
396
+ return false;
397
+ }
398
+ }
399
+
400
+ // src/token-store.ts
401
+ var exec2 = promisify(execFile);
340
402
  function assertKeychainSupported() {
341
403
  if (process.platform !== "darwin") {
342
404
  throw new Error(
343
- `@agentproto/auth token-store: the Keychain backend only supports macOS (got platform "${process.platform}"). Provide a libsecret (Linux) or Credential Manager (Windows) implementation to run here.`
405
+ `@agentproto/auth token-store: the Keychain backend only supports macOS and Windows (got platform "${process.platform}"). Provide a libsecret (Linux) implementation to run here.`
344
406
  );
345
407
  }
346
408
  }
@@ -349,9 +411,10 @@ function resolveAccount(account, server) {
349
411
  return account.replace("{server}", server);
350
412
  }
351
413
  async function readKeychainToken(service, account) {
414
+ if (process.platform === "win32") return readDpapiToken(service, account);
352
415
  assertKeychainSupported();
353
416
  try {
354
- const { stdout } = await exec("security", [
417
+ const { stdout } = await exec2("security", [
355
418
  "find-generic-password",
356
419
  "-s",
357
420
  service,
@@ -366,8 +429,9 @@ async function readKeychainToken(service, account) {
366
429
  }
367
430
  }
368
431
  async function writeKeychainToken(service, account, token) {
432
+ if (process.platform === "win32") return writeDpapiToken(service, account, token);
369
433
  assertKeychainSupported();
370
- await exec("security", [
434
+ await exec2("security", [
371
435
  "add-generic-password",
372
436
  "-U",
373
437
  "-s",
@@ -381,9 +445,10 @@ async function writeKeychainToken(service, account, token) {
381
445
  ]);
382
446
  }
383
447
  async function deleteKeychainToken(service, account) {
448
+ if (process.platform === "win32") return deleteDpapiToken(service, account);
384
449
  assertKeychainSupported();
385
450
  try {
386
- await exec("security", [
451
+ await exec2("security", [
387
452
  "delete-generic-password",
388
453
  "-s",
389
454
  service,
@@ -1183,6 +1248,54 @@ var CredentialBroker = class {
1183
1248
  return headers;
1184
1249
  }
1185
1250
  };
1251
+
1252
+ // src/subaccounts.ts
1253
+ var SUBACCOUNT_KIND_RE = /^[a-z][a-z0-9-]*$/;
1254
+ function parseSubaccountPin(spec) {
1255
+ const at = spec.indexOf(":");
1256
+ if (at <= 0) return void 0;
1257
+ const kind = spec.slice(0, at).trim();
1258
+ const id = spec.slice(at + 1).trim();
1259
+ if (!SUBACCOUNT_KIND_RE.test(kind) || id === "") return void 0;
1260
+ return { kind, id };
1261
+ }
1262
+ var SubaccountError = class extends Error {
1263
+ constructor(message) {
1264
+ super(message);
1265
+ this.name = "SubaccountError";
1266
+ }
1267
+ };
1268
+ var providers = /* @__PURE__ */ new Map();
1269
+ function registerSubaccountProvider(provider) {
1270
+ providers.set(provider.id, provider);
1271
+ }
1272
+ function unregisterSubaccountProvider(id) {
1273
+ return providers.delete(id);
1274
+ }
1275
+ function listSubaccountProviders() {
1276
+ return [...providers.values()];
1277
+ }
1278
+ function getSubaccountProvider(id) {
1279
+ return providers.get(id);
1280
+ }
1281
+ function findSubaccountProvider(account) {
1282
+ const all = listSubaccountProviders();
1283
+ if (account.source !== void 0) {
1284
+ return all.find((p) => p.source === account.source) ?? all.find((p) => p.migrateLegacySource?.(account.source) !== void 0);
1285
+ }
1286
+ if (account.endpoint !== void 0) return all.find((p) => p.endpoints?.includes(account.endpoint));
1287
+ return void 0;
1288
+ }
1289
+ function migrateLegacySubaccountProfile(profile) {
1290
+ if (profile.subaccount !== void 0 || profile.source === void 0) return profile;
1291
+ for (const provider of providers.values()) {
1292
+ const pin = provider.migrateLegacySource?.(profile.source);
1293
+ if (pin !== void 0 && provider.source !== void 0) {
1294
+ return { ...profile, source: provider.source, subaccount: pin };
1295
+ }
1296
+ }
1297
+ return profile;
1298
+ }
1186
1299
  var authMethodSchema = z.enum(["oauth-bearer", "api-key"]);
1187
1300
  var modelCurationSchema = z.object({
1188
1301
  mode: z.enum(["all", "allow"]),
@@ -1193,12 +1306,18 @@ var costBudgetSchema = z.object({
1193
1306
  window: z.string(),
1194
1307
  scope: z.enum(["session", "profile"])
1195
1308
  });
1309
+ var subaccountPinSchema = z.object({
1310
+ kind: z.string(),
1311
+ id: z.string(),
1312
+ name: z.string().optional()
1313
+ });
1196
1314
  var authProfileSchema = z.object({
1197
1315
  id: z.string(),
1198
1316
  vendor: z.string(),
1199
1317
  method: authMethodSchema,
1200
1318
  credentialRef: z.string().optional(),
1201
1319
  source: z.string().optional(),
1320
+ subaccount: subaccountPinSchema.optional(),
1202
1321
  label: z.string().optional(),
1203
1322
  // Additive, back-compat fields — an entry that predates them parses
1204
1323
  // unchanged (both optional), and `endpoint` still shadows the on-disk
@@ -1245,12 +1364,13 @@ async function writeAuthProfiles(file) {
1245
1364
  }
1246
1365
  async function listAuthProfiles(endpoint) {
1247
1366
  const file = await loadAuthProfiles();
1248
- const all = Object.values(file.profiles);
1367
+ const all = Object.values(file.profiles).map(migrateLegacySubaccountProfile);
1249
1368
  return endpoint === void 0 ? all : all.filter((p) => p.endpoint === endpoint);
1250
1369
  }
1251
1370
  async function getAuthProfile(id) {
1252
1371
  const file = await loadAuthProfiles();
1253
- return file.profiles[id];
1372
+ const profile = file.profiles[id];
1373
+ return profile ? migrateLegacySubaccountProfile(profile) : void 0;
1254
1374
  }
1255
1375
  async function addAuthProfile(profile) {
1256
1376
  const file = await loadAuthProfiles();
@@ -1295,13 +1415,49 @@ function validateCreateInput(input) {
1295
1415
  }
1296
1416
  const credential = input.credential !== void 0 ? input.credential.trim() : void 0;
1297
1417
  const source = input.source?.trim();
1298
- if (method === "api-key") {
1299
- if (source) {
1418
+ let subaccount;
1419
+ if (input.subaccount !== void 0) {
1420
+ const kind = (input.subaccount.kind ?? "").trim();
1421
+ const subId = (input.subaccount.id ?? "").trim();
1422
+ const subName = input.subaccount.name?.trim();
1423
+ if (!SUBACCOUNT_KIND_RE.test(kind) || !subId) {
1300
1424
  throw new AuthProfileValidationError(
1301
- "source is only supported for oauth-bearer profiles \u2014 api-key profiles require a credential"
1425
+ `subaccount must be "<kind>:<id>" with a lowercase kind (org, workspace, project, \u2026) and a non-empty id`
1302
1426
  );
1303
1427
  }
1304
- if (!credential) throw new AuthProfileValidationError("credential is required");
1428
+ subaccount = { kind, id: subId, ...subName ? { name: subName } : {} };
1429
+ if (!credential && !source) {
1430
+ throw new AuthProfileValidationError(
1431
+ "a subaccount pin needs a parent account \u2014 give a credential or a source"
1432
+ );
1433
+ }
1434
+ }
1435
+ const provider = source ? findSubaccountProvider({ source }) : void 0;
1436
+ if (subaccount && provider && !provider.kinds.includes(subaccount.kind)) {
1437
+ throw new AuthProfileValidationError(
1438
+ `subaccount kind "${subaccount.kind}" is not supported by "${provider.id}" (supported: ${provider.kinds.join(", ")})`
1439
+ );
1440
+ }
1441
+ if (method === "api-key") {
1442
+ if (source) {
1443
+ if (!provider) {
1444
+ throw new AuthProfileValidationError(
1445
+ `source is only supported for oauth-bearer profiles and for accounts owned by a registered sub-account provider \u2014 api-key profiles otherwise require a credential`
1446
+ );
1447
+ }
1448
+ if (!subaccount && provider.migrateLegacySource?.(source) === void 0) {
1449
+ throw new AuthProfileValidationError(
1450
+ `source "${source}" resolves an account with sub-accounts \u2014 pin one with a subaccount ("<kind>:<id>")`
1451
+ );
1452
+ }
1453
+ if (credential) {
1454
+ throw new AuthProfileValidationError(
1455
+ "give either credential or source, not both \u2014 a source-backed profile stores no secret"
1456
+ );
1457
+ }
1458
+ } else if (!credential) {
1459
+ throw new AuthProfileValidationError("credential is required");
1460
+ }
1305
1461
  } else {
1306
1462
  if (credential && source) {
1307
1463
  throw new AuthProfileValidationError(
@@ -1326,6 +1482,7 @@ function validateCreateInput(input) {
1326
1482
  method,
1327
1483
  ...credential ? { credential } : {},
1328
1484
  ...source ? { source } : {},
1485
+ ...subaccount ? { subaccount } : {},
1329
1486
  ...label ? { label } : {},
1330
1487
  ...credentialRef ? { credentialRef } : {},
1331
1488
  ...origin ? { origin } : {}
@@ -1358,20 +1515,22 @@ async function createAuthProfile(input, deps) {
1358
1515
  );
1359
1516
  }
1360
1517
  if (v.source !== void 0) {
1361
- const profile2 = {
1518
+ const profile2 = migrateLegacySubaccountProfile({
1362
1519
  id: v.id,
1363
1520
  endpoint: v.endpoint,
1364
1521
  method: v.method,
1365
1522
  source: v.source,
1523
+ ...v.subaccount ? { subaccount: v.subaccount } : {},
1366
1524
  ...v.label ? { label: v.label } : {},
1367
1525
  ...v.origin ? { origin: v.origin } : {}
1368
- };
1526
+ });
1369
1527
  await deps.addProfile(profile2);
1370
1528
  return {
1371
1529
  id: profile2.id,
1372
1530
  endpoint: profile2.endpoint,
1373
1531
  method: profile2.method,
1374
- source: v.source,
1532
+ source: profile2.source,
1533
+ ...profile2.subaccount ? { subaccount: profile2.subaccount } : {},
1375
1534
  ...profile2.label ? { label: profile2.label } : {},
1376
1535
  ...profile2.origin ? { origin: profile2.origin } : {}
1377
1536
  };
@@ -1392,6 +1551,7 @@ async function createAuthProfile(input, deps) {
1392
1551
  endpoint: v.endpoint,
1393
1552
  method: v.method,
1394
1553
  credentialRef,
1554
+ ...v.subaccount ? { subaccount: v.subaccount } : {},
1395
1555
  ...v.label ? { label: v.label } : {},
1396
1556
  ...v.origin ? { origin: v.origin } : {}
1397
1557
  };
@@ -1401,6 +1561,7 @@ async function createAuthProfile(input, deps) {
1401
1561
  endpoint: profile.endpoint,
1402
1562
  method: profile.method,
1403
1563
  credentialRef,
1564
+ ...v.subaccount ? { subaccount: v.subaccount } : {},
1404
1565
  ...profile.label ? { label: profile.label } : {},
1405
1566
  ...profile.origin ? { origin: profile.origin } : {},
1406
1567
  fingerprint: fingerprintCredential(credential)
@@ -1561,6 +1722,6 @@ function eligibleProfiles(profiles, manifest, route) {
1561
1722
  var SPEC_NAME = "agentauth";
1562
1723
  var SPEC_VERSION = "v1";
1563
1724
 
1564
- export { AuthProfileValidationError, BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, CredentialBroker, DiscoveryError, FLOW_ENGINES, FileStore, KeychainStore, MemoryStore, SPEC_NAME, SPEC_VERSION, addAuthProfile, authConfigSchema, authProfilesPath, authProviderFrontmatterSchema, createAuthProfile, credentialIdentity, defineAuthProvider, deleteAuthProfile, deleteKeychainToken, deriveCredentialRef, discoverEndpoints, eligibleProfiles, fingerprintCredential, getAuthProfile, getAuthProvider, guildeAuthProvider, installConfigSchema, listAuthProfiles, listAuthProviderIds, listAuthProviders, loadAuthProfiles, methodsPresentable, parseAuthProviderManifest, parseAuthProviderManifestRaw, readKeychainToken, refreshAuthProfileModels, registerAuthProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, setAuthProfileEnabled, setAuthProfileModels, tokenStoreSpecSchema, updateAuthProfile, validateCreateInput, writeKeychainToken };
1725
+ export { AuthProfileValidationError, BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, CredentialBroker, DiscoveryError, FLOW_ENGINES, FileStore, KeychainStore, MemoryStore, SPEC_NAME, SPEC_VERSION, SUBACCOUNT_KIND_RE, SubaccountError, addAuthProfile, authConfigSchema, authProfilesPath, authProviderFrontmatterSchema, createAuthProfile, credentialIdentity, defineAuthProvider, deleteAuthProfile, deleteKeychainToken, deriveCredentialRef, discoverEndpoints, eligibleProfiles, findSubaccountProvider, fingerprintCredential, getAuthProfile, getAuthProvider, getSubaccountProvider, guildeAuthProvider, installConfigSchema, listAuthProfiles, listAuthProviderIds, listAuthProviders, listSubaccountProviders, loadAuthProfiles, methodsPresentable, migrateLegacySubaccountProfile, parseAuthProviderManifest, parseAuthProviderManifestRaw, parseSubaccountPin, readKeychainToken, refreshAuthProfileModels, registerAuthProvider, registerSubaccountProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, setAuthProfileEnabled, setAuthProfileModels, tokenStoreSpecSchema, unregisterSubaccountProvider, updateAuthProfile, validateCreateInput, writeKeychainToken };
1565
1726
  //# sourceMappingURL=index.mjs.map
1566
1727
  //# sourceMappingURL=index.mjs.map