/* * Copyright 2021-2023 Lightbend Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Cloudevent } from './cloudevent'; import { JwtClaims } from './jwt-claims'; import * as protocol from '../types/protocol/commands'; const PrincipalsSource = '_kalix-src'; const PrincipalsService = '_kalix-src-svc'; /** * A metadata value. Can either be a string or a buffer. * * @public */ export type MetadataValue = string | Buffer; // Using an interface for compatibility with legacy JS code /** * A metadata entry. * * @public */ export interface MetadataEntry { /** * The key for this metadata entry. */ readonly key: string; /** * The entry value as bytes. */ readonly bytesValue: Buffer | undefined; /** * The entry value as a string. */ readonly stringValue: string | undefined; } /** @public */ export interface MetadataMap { [key: string]: string | Buffer | undefined; } class MetadataMapProxyHandler implements ProxyHandler { private metadata: Metadata; constructor(metadata: Metadata) { this.metadata = metadata; } ownKeys(target: MetadataMap): ArrayLike { const keys = new Array(); for (const entry of this.metadata.entries) { keys.push(entry.key); } return keys; } deleteProperty(target: MetadataMap, key: string | symbol): boolean { if (typeof key === 'string') { const hasKey = this.metadata.has(key as string); if (hasKey) { this.metadata.delete(key); } return hasKey; } return false; } get(target: MetadataMap, key: string | symbol, receiver: any): any { if (typeof key === 'string') { const lowercaseKey = (key as string).toLowerCase(); for (const entry of this.metadata.entries) { if (lowercaseKey === entry.key.toLowerCase()) { if (entry.stringValue) { return entry.stringValue; } if (entry.bytesValue) { return entry.bytesValue; } } } } return undefined; } has(target: MetadataMap, key: string | symbol): boolean { if (typeof key === 'string') { const lowercaseKey = (key as string).toLowerCase(); for (const entry of this.metadata.entries) { if (lowercaseKey === entry.key.toLowerCase()) { return true; } } } return false; } set( target: MetadataMap, key: string | symbol, value: any, receiver: any, ): boolean { if (typeof key === 'string') { this.metadata.delete(key as string); this.metadata.set(key as string, value); return true; } return false; } getOwnPropertyDescriptor( target: MetadataMap, key: string | symbol, ): PropertyDescriptor | undefined { // eslint-disable-next-line @typescript-eslint/no-this-alias const superThis = this; if (typeof key === 'string') { const v = this.get(target, key as string, null); if (v !== undefined) { return new (class implements PropertyDescriptor { readonly value = v; readonly writable = true; readonly enumerable = true; readonly configurable = false; get(): any { return v; } set(v: any): void { superThis.set(target, key, v, null); } })(); } } return undefined; } } /** * A principal associated with a request. * @public */ export type Principal = PredefinedPrincipal | LocalServicePrincipal; /** * Predefined principals * @public */ export enum PredefinedPrincipal { Internet, Self, Backoffice, } /** * A principal for a request from another service in the same Kalix Project * @public */ export class LocalServicePrincipal { /** * The name the requesting service is deployed as */ readonly name: string; constructor(name: string) { this.name = name; } } /** * The principals associated with a request. * @public */ export interface Principals { /** Whether this request was from the internet. */ isInternet(): boolean; /** Whether this is a self request. */ isSelf(): boolean; /** Whether this request is a backoffice request. */ isBackoffice(): boolean; /** * Whether this request was from a service in the local project. * * @param name The name of the service. */ isLocalService(name: string): boolean; /** Whether this request was from any service in the local project. */ isAnyLocalService(): boolean; /** Get the service that invoked this call, if any. */ getLocalService(): string | undefined; /** Get the principals associated with this request. */ get(): Array; } /** * Kalix metadata. * * Metadata is treated as case insensitive on lookup, and case sensitive on set. Multiple values per key are supported, * setting a value will add it to the current values for that key. You should delete first if you wish to replace a * value. * * Values can either be strings or byte buffers. If a non string or byte buffer value is set, it will be converted to * a string using toString. * * @param entries - the list of entries * * @public */ export class Metadata { readonly entries: MetadataEntry[]; /** * The metadata expressed as an object/map. * * @remarks * * The map is backed by the this Metadata object - changes to this map will be reflected in this metadata object and * changes to this object will be reflected in the map. * * The map will return the first metadata entry that matches the key, case insensitive, when properties are looked up. * When setting properties, it will replace all entries that match the key, case insensitive. */ readonly asMap: MetadataMap = new Proxy( new (class implements MetadataMap { [key: string]: string | Buffer | undefined; })(), new MetadataMapProxyHandler(this), ); /** * The Cloudevent data from this Metadata. * * @remarks * This object is backed by this Metadata, changes to the Cloudevent will be reflected in the Metadata. */ readonly cloudevent: Cloudevent = new Cloudevent(this); /** * The JWT claims, if there was a validated bearer token with this request. */ readonly jwtClaims: JwtClaims = new JwtClaims(this); constructor(entries: MetadataEntry[] = []) { this.entries = entries; } /** * Create Metadata from the SDK protocol. * * @param metadata - protocol Metadata * @returns created Metadata * * @internal */ static fromProtocol(metadata?: protocol.Metadata | null): Metadata { if (metadata && metadata.entries) { const entries: MetadataEntry[] = metadata.entries.map((entry) => ({ key: entry.key ?? '', bytesValue: entry.bytesValue ? Buffer.from(entry.bytesValue) : undefined, stringValue: entry.stringValue ?? undefined, })); return new Metadata(entries); } else { return new Metadata(); } } /** * Set the HTTP status code for the response when sending a successful response using HTTP transcoding. * * @remarks * This will only apply to responses that are being transcoded to plain HTTP from gRPC using the protobuf HTTP * annotations. When gRPC is being used, calling this has no effect. * * @param code - The HTTP status code to set */ setHttpStatusCode(code: number): void { if (code < 100 || code >= 600) { throw new Error('Invalid HTTP status code: ' + code); } this.set('_kalix-http-code', code.toString()); } /** * Get the CloudEvent subject from the metadata. * * @returns CloudEvent subject value * * @deprecated Use {@link Cloudevent.subject} via {@link Metadata.cloudevent} instead. */ getSubject(): MetadataValue | undefined { const subject = this.get('ce-subject'); if (subject.length > 0) { return subject[0]; } else { return undefined; } } /** * Create a new MetadataEntry. * * @param key - the key for the entry * @param value - the value for the entry * @returns a new MetadataEntry */ private createMetadataEntry(key: string, value: any): MetadataEntry { if (typeof value === 'string') { return { key: key, stringValue: value, bytesValue: undefined }; } else if (Buffer.isBuffer(value)) { return { key: key, bytesValue: value, stringValue: undefined }; } else { return { key: key, stringValue: value.toString(), bytesValue: undefined }; } } /** * Get the value from a metadata entry. * * @param entry - the metadata entry * @returns the value for the given entry */ private getValue(entry: MetadataEntry): MetadataValue | undefined { if (entry.bytesValue !== undefined) { return entry.bytesValue; } else { return entry.stringValue; } } /** * Get all the values for the given key. * * @remarks * The key is case insensitive. * * @param key - the key to get * @returns all the values, or an empty array if no values exist for the key */ get(key: string): MetadataValue[] { const values: MetadataValue[] = []; this.entries.forEach((entry) => { if (key.toLowerCase() === entry.key.toLowerCase()) { const value = this.getValue(entry); if (value) { values.push(value); } } }); return values; } /** * Set a given key value. * * @remarks * This will append the key value to the metadata, it won't replace any existing values for existing keys. * * @param key - the key to set * @param value - the value to set * @returns this updated metadata */ set(key: string, value: any): Metadata { this.entries.push(this.createMetadataEntry(key, value)); return this; } /** * Delete all values with the given key. * * @remarks * The key is case insensitive. * * @param key - the key to delete * @returns this updated metadata */ delete(key: string) { let idx = 0; while (idx < this.entries.length) { const entry = this.entries[idx]; if (key.toLowerCase() !== entry.key.toLowerCase()) { idx++; } else { this.entries.splice(idx, 1); } } return this; } /** * Whether there exists a metadata value for the given key. * * @remarks * The key is case insensitive. * * @param key - the key to check * @returns whether values exist for the given key */ has(key: string): boolean { for (const idx in this.entries) { const entry = this.entries[idx]; if (key.toLowerCase() === entry.key.toLowerCase()) { return true; } } return false; } /** * Clear the metadata. * * @returns this updated metadata */ clear() { this.entries.splice(0, this.entries.length); return this; } /** * Get the Principals associated with this request. */ principals(): Principals { const sourceMeta = this.get(PrincipalsSource); let source: string | undefined = undefined; if (sourceMeta.length > 0) { source = sourceMeta[0] as string; } const serviceMeta = this.get(PrincipalsService); let service: string | undefined = undefined; if (serviceMeta.length > 0) { service = serviceMeta[0] as string; } return new (class implements Principals { get(): Array { const principals = []; switch (source) { case 'internet': principals.push(PredefinedPrincipal.Internet); break; case 'backoffice': principals.push(PredefinedPrincipal.Backoffice); break; case 'self': principals.push(PredefinedPrincipal.Self); break; } if (service) { principals.push(new LocalServicePrincipal(service)); } return principals; } getLocalService(): string | undefined { return service; } isAnyLocalService(): boolean { return service !== undefined; } isBackoffice(): boolean { return source == 'backoffice'; } isInternet(): boolean { return source == 'internet'; } isLocalService(name: string): boolean { return service == name; } isSelf(): boolean { return source == 'self'; } })(); } }