| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- /**
- * Host owner of the `credentials` Remote namespace: the reference half of
- * `ctx.credentials` as a browser configuration page reads and writes it.
- *
- * @module @deepseek-ai/dsh-api-settings-controller/src/credentials.ts
- */
- import { Context } from '@deepseek-ai/cordis'
- import { credentialRef } from '@deepseek-ai/dsh-credentials'
- import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'
- import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
- import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
- import { z } from 'zod'
- /**
- * Fan-out bound on one remote `describe` batch. A settings page asks about the
- * references its own rows name, so this is far above any real page and still
- * keeps one authenticated request from starting unbounded provider work.
- */
- const MAX_DESCRIBE_REFS = 64
- const credentialRefSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)
- const describeRequestSchema = z.object({
- refs: z.array(credentialRefSchema).max(MAX_DESCRIBE_REFS),
- })
- const setRequestSchema = z.object({ ref: credentialRefSchema, value: z.string().min(1) })
- const unsetRequestSchema = z.object({ ref: credentialRefSchema })
- /** Parse the domain constraints that are more specific than generated TypeScript codecs. */
- function parseRequest<T>(method: string, schema: z.ZodType<T>, value: unknown): T {
- const parsed = schema.safeParse(value)
- if (!parsed.success) {
- throw new RemoteError('gateway/bad-request', `invalid payload for ${method}`, { issues: parsed.error.issues })
- }
- return parsed.data
- }
- /**
- * Copy exactly the fields {@link CredentialInfo} declares. The Gateway returns
- * a business result without decoding it, so a provider whose `describe` carried
- * extra enumerable properties would otherwise serialize them to the caller.
- * @param info - the provider's answer for one reference.
- * @returns the same facts with nothing else attached.
- */
- function projectCredentialInfo(info: CredentialInfo): CredentialInfo {
- return {
- configured: info.configured,
- ...info.source === undefined ? {} : { source: info.source },
- writable: info.writable,
- }
- }
- declare module '@deepseek-ai/cordis' {
- interface Context {
- /** Host owner of the `credentials` Remote namespace. */
- credentialsController: CredentialsController
- }
- }
- /**
- * Host service backing the generated `ctx.remote.credentials` namespace. It
- * carries every wire obligation the credential seam itself does not: the batch
- * fan-out bound, the field-by-field view projection, the reference-grammar
- * guard, and the refusal mapping. Secret values cross in one direction only —
- * no method here returns one.
- */
- export class CredentialsController extends TypertRemoteService {
- /** @param ctx - Host context where a credential provider may be mounted. */
- constructor(ctx: Context) {
- super(ctx, 'credentialsController', { namespace: 'credentials' })
- }
- /**
- * Describe several references for one configuration surface. Batched because
- * a settings page describes every reference its rows name at once, and one
- * round trip keeps those rows from settling separately.
- * @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar
- * rejects the whole call as `gateway/bad-request`.
- * @returns one view per requested name, keyed by that name.
- * @throws RemoteError when the request is invalid or no credential provider is mounted.
- */
- @Remote
- async describe(refs: string[]): Promise<Record<string, CredentialInfo>> {
- const request = parseRequest('credentials.describe', describeRequestSchema, { refs })
- const branded = request.refs.map(ref => [ref, credentialRef(ref)] as const)
- const credentials = this.provider()
- const entries = await Promise.all(branded.map(async ([ref, key]) =>
- [ref, projectCredentialInfo(await credentials.describe(key))] as const))
- return Object.fromEntries(entries)
- }
- /**
- * Store one value from a configuration surface. The value crosses the wire in
- * this direction only: no read path returns it.
- * @param ref - reference name to store under.
- * @param value - the non-empty secret value.
- * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
- */
- @Remote
- async set(ref: string, value: string): Promise<void> {
- const request = parseRequest('credentials.set', setRequestSchema, { ref, value })
- const branded = credentialRef(request.ref)
- const credentials = this.provider()
- await this.write(request.ref, () => credentials.set(branded, request.value))
- }
- /**
- * Remove one reference from a configuration surface.
- * @param ref - reference name to remove.
- * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
- */
- @Remote
- async unset(ref: string): Promise<void> {
- const request = parseRequest('credentials.unset', unsetRequestSchema, { ref })
- const branded = credentialRef(request.ref)
- const credentials = this.provider()
- await this.write(request.ref, () => credentials.unset(branded))
- }
- /** Resolve the optional provider or report how to supply it. */
- private provider(): CredentialProvider {
- const credentials = this.ctx.get('credentials')
- if (credentials === undefined) {
- throw new RemoteError(
- 'gateway/internal',
- 'credentials service is absent: this deployment does not mount a credential provider (e.g. @deepseek-ai/dsh-credentials-local) in its composition',
- {},
- )
- }
- return credentials
- }
- /**
- * Run one remote write and report every refusal as `credential/rejected`
- * carrying the seam's own message: a read-only source shadowing the reference
- * is what a configuration surface must show verbatim. Callers brand the
- * reference before entering, so a name outside the grammar never reaches this
- * path and fails the same way it does on the read side. The details name only
- * the reference, so no failure path can carry the value back out.
- */
- private async write(ref: string, write: () => Promise<void>): Promise<void> {
- try {
- await write()
- } catch (error: unknown) {
- throw new RemoteError(
- 'credential/rejected',
- error instanceof Error ? error.message : String(error),
- { ref },
- { cause: error },
- )
- }
- }
- }
- export default CredentialsController
|