File size: 6,483 Bytes
09aec41 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | /**
* 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
|