@stacksjs/support 0.0.0-stage → 0.75.580
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/LICENSE.md +21 -0
- package/README.md +46 -2
- package/dist/browser.d.ts +38 -0
- package/dist/browser.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +1 -0
- package/dist/testing.d.ts +15 -0
- package/dist/testing.js +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +0 -0
- package/package.json +53 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @stacksjs/support
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native HQ.support integration for server identity, customer events, conversations,
|
|
4
|
+
and the compiled stx messenger. The package has no runtime dependencies.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createSupportClient } from '@stacksjs/support/server'
|
|
8
|
+
|
|
9
|
+
const support = createSupportClient({
|
|
10
|
+
key: process.env.SUPPORTHQ_KEY!,
|
|
11
|
+
readKey: process.env.SUPPORTHQ_READ_KEY,
|
|
12
|
+
project: process.env.SUPPORTHQ_PROJECT_ID!,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// Derive identity from your authenticated server request, never browser input.
|
|
16
|
+
const session = await support.createSession({ externalId: String(user.id), name: user.name })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keep integration and read keys on the server. Give the browser only the public
|
|
20
|
+
project identifier and returned short-lived customer session. Connect using
|
|
21
|
+
`buddy hq:connect supporthq`; the service origin defaults to `https://hq.support`.
|
|
22
|
+
|
|
23
|
+
Writes require an idempotency key. Reuse the same key for the same operation
|
|
24
|
+
after an uncertain result. The client does not automatically retry writes: a
|
|
25
|
+
network failure can happen after the server commits. `SupportError` reports a
|
|
26
|
+
safe code and HTTP status, without including provider bodies or credentials.
|
|
27
|
+
|
|
28
|
+
`conversations.list` uses the read key where supplied and returns scoped summaries,
|
|
29
|
+
not private transcript access. A service URL uses HTTPS except local development
|
|
30
|
+
on localhost. Authentication is never followed across redirects.
|
|
31
|
+
|
|
32
|
+
The browser subpath imports neither server credential handling nor DOM globals:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createSupportBrowser } from '@stacksjs/support/browser'
|
|
36
|
+
|
|
37
|
+
// Load the compiled stx element and obtain its instance in your host integration.
|
|
38
|
+
const support = createSupportBrowser(messengerElement)
|
|
39
|
+
await support.boot({ project: 'project-id', sessionToken: session.token })
|
|
40
|
+
support.open()
|
|
41
|
+
// Always clear customer state during logout and account or workspace changes.
|
|
42
|
+
support.shutdown()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Load the web component assets separately. The lifecycle wrapper accepts the
|
|
46
|
+
element's documented methods and does not inject scripts, add HTML attributes,
|
|
47
|
+
or depend on a Stacks page's ambient bindings.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Customer credentials remain properties, never reflected into HTML attributes. */
|
|
2
|
+
export declare function createSupportBrowser(widget: SupportWidget): SupportBrowser;
|
|
3
|
+
/** Opt-in native layout bridge. Every refresh clears the previous customer's state first. */
|
|
4
|
+
export declare function mountNativeSupport(container: HTMLElement, options?: NativeSupportMountOptions): NativeSupportMount;
|
|
5
|
+
/** The compiled stx messenger supplies this interface; no page globals are used. */
|
|
6
|
+
export declare interface SupportWidget {
|
|
7
|
+
boot: (options: { url: string, project: string, token?: string, locale?: string }) => Promise<void>
|
|
8
|
+
open: () => void
|
|
9
|
+
close: () => void
|
|
10
|
+
update: (options: { token?: string, locale?: string }) => void | Promise<void>
|
|
11
|
+
track: (event: string, attributes?: Record<string, string | number | boolean | null>) => Promise<unknown>
|
|
12
|
+
shutdown: () => void
|
|
13
|
+
}
|
|
14
|
+
export declare interface BrowserBootOptions {
|
|
15
|
+
endpoint?: string
|
|
16
|
+
project: string
|
|
17
|
+
sessionToken?: string
|
|
18
|
+
locale?: string
|
|
19
|
+
}
|
|
20
|
+
export declare interface SupportBrowser {
|
|
21
|
+
boot: (options: BrowserBootOptions) => Promise<void>
|
|
22
|
+
open: () => void
|
|
23
|
+
close: () => void
|
|
24
|
+
update: (options: { sessionToken?: string, locale?: string }) => Promise<void>
|
|
25
|
+
track: (event: string, attributes?: Record<string, string | number | boolean | null>) => Promise<void>
|
|
26
|
+
shutdown: () => void
|
|
27
|
+
}
|
|
28
|
+
export declare interface NativeSupportMount {
|
|
29
|
+
refresh: () => Promise<void>
|
|
30
|
+
shutdown: () => void
|
|
31
|
+
}
|
|
32
|
+
export declare interface NativeSupportMountOptions {
|
|
33
|
+
getToken?: () => string | null | undefined
|
|
34
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>
|
|
35
|
+
load?: (url: string) => Promise<{ createSupport: (options: { container: HTMLElement, inline?: boolean }) => SupportBrowser }>
|
|
36
|
+
events?: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>
|
|
37
|
+
onError?: (code: 'unavailable') => void
|
|
38
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createSupportBrowser(widget){let booted=false;let generation=0;let booting=false;function requireBoot(){if(!booted)throw new Error("HQ.support messenger is not booted.")}return{async boot(options){if(!options.project?.trim())throw new Error("HQ.support project is required.");if(booting)throw new Error("HQ.support messenger is already booting.");if(booted)widget.shutdown();booted=false;booting=true;const current=++generation;try{await widget.boot({url:options.endpoint??"https://hq.support",project:options.project,token:options.sessionToken,locale:options.locale});if(current===generation)booted=true;else widget.shutdown()}catch(error){widget.shutdown();throw error}finally{booting=false}},open(){requireBoot();widget.open()},close(){requireBoot();widget.close()},async update(options){requireBoot();await widget.update({token:options.sessionToken,locale:options.locale})},async track(event,attributes){requireBoot();if(!event.trim())throw new Error("HQ.support event is required.");await widget.track(event,attributes)},shutdown(){if(!booted&&!booting)return;generation++;booted=false;widget.shutdown()}}}export function mountNativeSupport(container,options={}){const fetcher=options.fetch??globalThis.fetch;const load=options.load??(async(url)=>await import(url));const events=options.events??(typeof globalThis.addEventListener==="function"?globalThis:undefined);let active,epoch=0,disposed=false,abort;function clear(){epoch++;abort?.abort();active?.shutdown();active=undefined}const controller={async refresh(){if(disposed)return;clear();const current=epoch;abort=new AbortController;const init={credentials:"same-origin",cache:"no-store",redirect:"error",signal:AbortSignal.any([abort.signal,AbortSignal.timeout(1e4)])};try{const response=await fetcher("/api/support/config",init);if(!response.ok)return;const config=await response.json();if(!config||typeof config!=="object")throw new Error("Configuration unavailable");const value=config;if(value.enabled!==true)return;if(typeof value.project!=="string"||!value.project||value.sessionEndpoint!=="/api/support/session"||typeof value.endpoint!=="string"||typeof value.loaderUrl!=="string")throw new Error("Configuration unavailable");for(const input of[value.endpoint,value.loaderUrl]){const url=new URL(input);if(url.username||url.password||url.hash||url.search||url.protocol!=="https:"&&!(url.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(url.hostname)))throw new Error("Invalid support origin")}const csrfResponse=await fetcher("/api/csrf-token",init);if(!csrfResponse.ok)throw new Error("Session unavailable");const csrf=await csrfResponse.json();if(typeof csrf.token!=="string")throw new Error("Session unavailable");const token=options.getToken?.();const sessionResponse=await fetcher("/api/support/session",{...init,method:"POST",headers:{Accept:"application/json","x-csrf-token":csrf.token,...token?{Authorization:`Bearer ${token}`}:{}}});if(sessionResponse.status===401||sessionResponse.status===403)return;if(!sessionResponse.ok)throw new Error("Session unavailable");const session=await sessionResponse.json();if(typeof session.token!=="string"||!session.token)throw new Error("Session unavailable");if(disposed||current!==epoch)return;const module=await load(value.loaderUrl);if(disposed||current!==epoch)return;const next=module.createSupport({container,inline:false});active=next;await next.boot({project:value.project,endpoint:value.endpoint,sessionToken:session.token,locale:typeof value.locale==="string"?value.locale:"en"});if(disposed||current!==epoch)next.shutdown()}catch{if(!disposed&¤t===epoch){clear();options.onError?.("unavailable")}}},shutdown(){disposed=true;clear();for(const name of["stx:navigate","stacks:auth-changed"])events?.removeEventListener(name,refresh);events?.removeEventListener("storage",storage);events?.removeEventListener("pagehide",clear);events?.removeEventListener("stacks:auth-changing",clear)}};const refresh=()=>{controller.refresh()};const storage=(event)=>{const key=event.key;if(key===null||/auth|token|session/i.test(key))refresh()};for(const name of["stx:navigate","stacks:auth-changed"])events?.addEventListener(name,refresh);events?.addEventListener("storage",storage);events?.addEventListener("pagehide",clear);events?.addEventListener("stacks:auth-changing",clear);return controller}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{createAuthenticatedSupportSession,createSupportClient,SupportError}from"./server";
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CustomerSession, SupportClient, SupportClientOptions, SupportErrorCode } from './types';
|
|
2
|
+
export type * from './types';
|
|
3
|
+
/** No automatic write retries: a timed-out request may already have committed. */
|
|
4
|
+
export declare function createSupportClient(options: SupportClientOptions): SupportClient;
|
|
5
|
+
/** Identity is read from the authenticated server request, never a submitted user ID. */
|
|
6
|
+
export declare function createAuthenticatedSupportSession(client: Pick<SupportClient, 'createSession'>, request: AuthenticatedSupportRequest, options?: {
|
|
7
|
+
expose?: readonly ('email' | 'name')[]
|
|
8
|
+
|
|
9
|
+
resolveTenant?: (request: AuthenticatedSupportRequest) => string | number | Promise<string | number>
|
|
10
|
+
}): Promise<CustomerSession>;
|
|
11
|
+
export declare interface AuthenticatedSupportRequest {
|
|
12
|
+
user: () => Promise<{ id: number | string, email?: unknown, name?: unknown } | null | undefined>
|
|
13
|
+
}
|
|
14
|
+
/** A safe error: provider bodies and credentials never become error messages. */
|
|
15
|
+
export declare class SupportError extends Error {
|
|
16
|
+
public readonly code: SupportErrorCode;
|
|
17
|
+
public readonly status: number;
|
|
18
|
+
constructor(code: SupportErrorCode, status?: number);
|
|
19
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class SupportError extends Error{code;status;constructor(code,status=0){super(`HQ.support request failed (${code}).`);this.code=code;this.status=status;this.name="SupportError"}}function record(value){return value!==null&&typeof value==="object"&&!Array.isArray(value)}function nonEmpty(value,max=255){return typeof value==="string"&&value.trim().length>0&&value.length<=max}function validate(value,max=255){if(!nonEmpty(value,max))throw new SupportError("validation")}function serviceUrl(input){let url;try{url=new URL(input)}catch{throw new SupportError("configuration")}const local=["localhost","127.0.0.1","[::1]"].includes(url.hostname);if(url.username||url.password||url.search||url.hash||url.protocol!=="https:"&&!(local&&url.protocol==="http:"))throw new SupportError("configuration");return url.toString().replace(/\/$/,"")}function summary(value){return record(value)&&nonEmpty(value.id)&&typeof value.subject==="string"&&nonEmpty(value.status)&&nonEmpty(value.customer_id)&&nonEmpty(value.updated_at)&&Number.isSafeInteger(value.unread_count)&&value.unread_count>=0}function session(value){return record(value)&&nonEmpty(value.token,16384)&&nonEmpty(value.expiresAt)&&Number.isFinite(Date.parse(value.expiresAt))&&record(value.contact)&&nonEmpty(value.contact.id)}function message(value){return record(value)&&nonEmpty(value.id)&&nonEmpty(value.conversation_id)&&typeof value.body==="string"&&(value.visibility==="public"||value.visibility==="internal")&&nonEmpty(value.created_at)}function page(value){return record(value)&&Array.isArray(value.conversations)&&value.conversations.every(summary)&&(value.nextCursor===null||typeof value.nextCursor==="string")}function statusCode(status){if(status===401)return"unauthorized";if(status===403)return"forbidden";if(status===404)return"not_found";if(status===409)return"conflict";if(status===429||status===402)return"quota_exceeded";if(status===400||status===422)return"validation";return"unavailable"}export function createSupportClient(options){if(!nonEmpty(options.key,4096)||!nonEmpty(options.project)||options.readKey!==undefined&&!nonEmpty(options.readKey,4096))throw new SupportError("configuration");const base=`${serviceUrl(options.url??"https://hq.support")}/api/v1/projects/${encodeURIComponent(options.project)}`;const fetcher=options.fetch??globalThis.fetch;const timeoutMs=options.timeoutMs??1e4;if(!Number.isSafeInteger(timeoutMs)||timeoutMs<=0||timeoutMs>120000)throw new SupportError("configuration");async function request(path,check,input,retryKey,read=false){const headers=new Headers({Accept:"application/json",Authorization:`Bearer ${read?options.readKey??options.key:options.key}`});if(input!==undefined)headers.set("Content-Type","application/json");if(retryKey)headers.set("Idempotency-Key",retryKey);const controller=new AbortController;const timer=setTimeout(()=>controller.abort(),timeoutMs);try{const response=await fetcher(`${base}${path}`,{method:input===undefined?"GET":"POST",headers,signal:controller.signal,body:input===undefined?undefined:JSON.stringify(input),redirect:"error"});if(!response.ok){await response.body?.cancel();throw new SupportError(statusCode(response.status),response.status)}let value;try{value=await response.json()}catch{throw new SupportError("bad_response",response.status)}if(!check(value))throw new SupportError("bad_response",response.status);return value}catch(error){if(error instanceof SupportError)throw error;throw new SupportError(controller.signal.aborted?"timeout":"network")}finally{clearTimeout(timer)}}return{createSession(identity){validate(identity.externalId);return request("/sessions",session,identity)},track(event){validate(event.externalId);validate(event.event);validate(event.idempotencyKey);return request("/events",(value)=>record(value)&&nonEmpty(value.id)&&value.accepted===true,event,event.idempotencyKey)},conversations:{create(input){validate(input.externalId);validate(input.subject);validate(input.body,20000);validate(input.idempotencyKey);return request("/conversations",(value)=>record(value)&&summary(value.conversation),input,input.idempotencyKey)},reply(id,input){validate(id);validate(input.body,20000);validate(input.idempotencyKey);if(!["public","internal"].includes(input.visibility))throw new SupportError("validation");return request(`/conversations/${encodeURIComponent(id)}/messages`,(value)=>record(value)&&message(value.message),input,input.idempotencyKey)},list(query={}){const params=new URLSearchParams;if(query.cursor!==undefined){validate(query.cursor,2048);params.set("cursor",query.cursor)}if(query.status!==undefined){validate(query.status);params.set("status",query.status)}if(query.limit!==undefined){if(!Number.isSafeInteger(query.limit)||query.limit<1||query.limit>100)throw new SupportError("validation");params.set("limit",String(query.limit))}return request(`/conversations${params.size?`?${params}`:""}`,page,undefined,undefined,true)}}}}export async function createAuthenticatedSupportSession(client,request,options={}){const user=await request.user();if(!user||!["string","number"].includes(typeof user.id)||typeof user.id==="number"&&!Number.isSafeInteger(user.id)||!String(user.id).trim())throw new SupportError("unauthorized",401);let externalId=`user:${String(user.id)}`;if(options.resolveTenant){const tenant=await options.resolveTenant(request);if(!["string","number"].includes(typeof tenant)||typeof tenant==="number"&&!Number.isSafeInteger(tenant)||!String(tenant).trim())throw new SupportError("forbidden",403);externalId=`tenant:${encodeURIComponent(String(tenant))}:user:${encodeURIComponent(String(user.id))}`}const identity={externalId,...options.expose?.includes("email")&&typeof user.email==="string"?{email:user.email}:{},...options.expose?.includes("name")&&typeof user.name==="string"?{name:user.name}:{}};return await client.createSession(identity)}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SupportFetch } from './types';
|
|
2
|
+
/** Test-only HTTP adapter. No port, database or global fetch replacement. */
|
|
3
|
+
export declare function createFakeSupportService(options?: FakeSupportOptions): FakeSupportService;
|
|
4
|
+
export declare interface FakeSupportOptions {
|
|
5
|
+
project?: string
|
|
6
|
+
key?: string
|
|
7
|
+
readKey?: string
|
|
8
|
+
now?: () => Date
|
|
9
|
+
}
|
|
10
|
+
export declare interface FakeSupportService {
|
|
11
|
+
project: string
|
|
12
|
+
key: string
|
|
13
|
+
readKey: string
|
|
14
|
+
fetch: SupportFetch
|
|
15
|
+
}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function canonical(value){if(Array.isArray(value))return`[${value.map(canonical).join(",")}]`;if(value!==null&&typeof value==="object"){const row=value;return`{${Object.keys(row).sort().map((key)=>`${JSON.stringify(key)}:${canonical(row[key])}`).join(",")}}`}return JSON.stringify(value)??"null"}export function createFakeSupportService(options={}){const project=options.project??"test-project";const key=options.key??"test-server-key";const readKey=options.readKey??"test-read-key";const now=options.now??(()=>new Date);const contacts=new Map;const conversations=new Map;const retry=new Map;let sequence=0;const next=(kind)=>`${kind}-${++sequence}`;const fetch=async(input,init)=>{const request=new Request(input,init);const url=new URL(request.url);const prefix=`/api/v1/projects/${encodeURIComponent(project)}`;if(!url.pathname.startsWith(`${prefix}/`))return Response.json({code:"not_found"},{status:404});const auth=request.headers.get("Authorization");const read=request.method==="GET";if(auth!==`Bearer ${key}`&&!(read&&auth===`Bearer ${readKey}`))return Response.json({code:"unauthorized"},{status:401});const path=url.pathname.slice(prefix.length);if(read&&path==="/conversations"){const rows=[...conversations.values()].filter((row)=>!url.searchParams.get("status")||row.status===url.searchParams.get("status"));return Response.json({conversations:rows.slice(0,Number(url.searchParams.get("limit")??100)),nextCursor:null})}if(request.method!=="POST")return Response.json({code:"not_found"},{status:404});let body;try{body=await request.json()}catch{return Response.json({code:"validation"},{status:422})}if(!body||typeof body!=="object"||Array.isArray(body))return Response.json({code:"validation"},{status:422});if(path==="/sessions"){if(typeof body.externalId!=="string")return Response.json({code:"validation"},{status:422});let contact=contacts.get(body.externalId);if(!contact){contact={id:next("contact"),externalId:body.externalId};contacts.set(body.externalId,contact)}return Response.json({token:next("session"),expiresAt:new Date(now().getTime()+900000).toISOString(),contact})}const retryKey=request.headers.get("Idempotency-Key");if(!retryKey)return Response.json({code:"validation"},{status:422});const scopedKey=`${path}:${retryKey}`;const fingerprint=canonical(body);const prior=retry.get(scopedKey);if(prior)return prior.body===fingerprint?Response.json(prior.answer):Response.json({code:"conflict"},{status:409});let answer;if(path==="/events"){answer={id:next("event"),accepted:true}}else if(path==="/conversations"){if(typeof body.externalId!=="string"||typeof body.subject!=="string")return Response.json({code:"validation"},{status:422});const contact=contacts.get(body.externalId);if(!contact)return Response.json({code:"not_found"},{status:404});const conversation={id:next("conversation"),subject:body.subject,status:"open",customer_id:contact.id,updated_at:now().toISOString(),unread_count:1};conversations.set(conversation.id,conversation);answer={conversation}}else{const match=path.match(/^\/conversations\/([^/]+)\/messages$/);if(!match||!conversations.has(decodeURIComponent(match[1])))return Response.json({code:"not_found"},{status:404});if(typeof body.body!=="string"||!["public","internal"].includes(String(body.visibility)))return Response.json({code:"validation"},{status:422});const message={id:next("message"),conversation_id:decodeURIComponent(match[1]),body:body.body,visibility:body.visibility,created_at:now().toISOString()};answer={message}}retry.set(scopedKey,{body:fingerprint,answer});return Response.json(answer,{status:201})};return{project,key,readKey,fetch}}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export declare interface SupportIdentity {
|
|
2
|
+
externalId: string
|
|
3
|
+
email?: string
|
|
4
|
+
name?: string
|
|
5
|
+
company?: { id: string, name?: string }
|
|
6
|
+
attributes?: Record<string, string | number | boolean | null>
|
|
7
|
+
}
|
|
8
|
+
export declare interface CustomerSession {
|
|
9
|
+
token: string
|
|
10
|
+
expiresAt: string
|
|
11
|
+
contact: { id: string, externalId?: string, name?: string, email?: string }
|
|
12
|
+
}
|
|
13
|
+
export declare interface ConversationSummary {
|
|
14
|
+
id: string
|
|
15
|
+
subject: string
|
|
16
|
+
status: string
|
|
17
|
+
customer_id: string
|
|
18
|
+
updated_at: string
|
|
19
|
+
unread_count: number
|
|
20
|
+
}
|
|
21
|
+
export declare interface ConversationPage {
|
|
22
|
+
conversations: ConversationSummary[]
|
|
23
|
+
nextCursor: string | null
|
|
24
|
+
}
|
|
25
|
+
export declare interface ConversationQuery {
|
|
26
|
+
cursor?: string
|
|
27
|
+
status?: string
|
|
28
|
+
limit?: number
|
|
29
|
+
}
|
|
30
|
+
export declare interface CreateConversation {
|
|
31
|
+
externalId: string
|
|
32
|
+
subject: string
|
|
33
|
+
body: string
|
|
34
|
+
idempotencyKey: string
|
|
35
|
+
}
|
|
36
|
+
export declare interface ReplyInput {
|
|
37
|
+
body: string
|
|
38
|
+
visibility: 'public' | 'internal'
|
|
39
|
+
idempotencyKey: string
|
|
40
|
+
}
|
|
41
|
+
export declare interface SupportMessage {
|
|
42
|
+
id: string
|
|
43
|
+
conversation_id: string
|
|
44
|
+
body: string
|
|
45
|
+
visibility: 'public' | 'internal'
|
|
46
|
+
created_at: string
|
|
47
|
+
}
|
|
48
|
+
export declare interface SupportEvent {
|
|
49
|
+
externalId: string
|
|
50
|
+
event: string
|
|
51
|
+
attributes?: Record<string, string | number | boolean | null>
|
|
52
|
+
idempotencyKey: string
|
|
53
|
+
}
|
|
54
|
+
export declare interface SupportClientOptions {
|
|
55
|
+
url?: string
|
|
56
|
+
key: string
|
|
57
|
+
readKey?: string
|
|
58
|
+
project: string
|
|
59
|
+
timeoutMs?: number
|
|
60
|
+
fetch?: SupportFetch
|
|
61
|
+
}
|
|
62
|
+
export declare interface SupportClient {
|
|
63
|
+
createSession: (identity: SupportIdentity) => Promise<CustomerSession>
|
|
64
|
+
track: (event: SupportEvent) => Promise<{ id: string, accepted: true }>
|
|
65
|
+
conversations: {
|
|
66
|
+
create: (input: CreateConversation) => Promise<{ conversation: ConversationSummary }>
|
|
67
|
+
reply: (id: string, input: ReplyInput) => Promise<{ message: SupportMessage }>
|
|
68
|
+
list: (query?: ConversationQuery) => Promise<ConversationPage>
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export type SupportErrorCode = 'configuration' | 'validation' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'quota_exceeded' | 'unavailable' | 'timeout' | 'network' | 'bad_response';
|
|
72
|
+
export type SupportFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/support",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.75.580",
|
|
5
|
+
"description": "Native HQ.support customer identity, conversations, events, and messenger lifecycle.",
|
|
6
|
+
"author": "Chris <chris@stacksjs.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
11
|
+
"directory": "storage/framework/core/support"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://hq.support/docs/stacks",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"bun": "./dist/index.js",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./server": {
|
|
22
|
+
"types": "./dist/server.d.ts",
|
|
23
|
+
"bun": "./dist/server.js",
|
|
24
|
+
"import": "./dist/server.js",
|
|
25
|
+
"default": "./dist/server.js"
|
|
26
|
+
},
|
|
27
|
+
"./browser": {
|
|
28
|
+
"types": "./dist/browser.d.ts",
|
|
29
|
+
"bun": "./dist/browser.js",
|
|
30
|
+
"import": "./dist/browser.js",
|
|
31
|
+
"default": "./dist/browser.js"
|
|
32
|
+
},
|
|
33
|
+
"./testing": {
|
|
34
|
+
"types": "./dist/testing.d.ts",
|
|
35
|
+
"bun": "./dist/testing.js",
|
|
36
|
+
"import": "./dist/testing.js",
|
|
37
|
+
"default": "./dist/testing.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"module": "dist/index.js",
|
|
41
|
+
"types": "dist/index.d.ts",
|
|
42
|
+
"files": [
|
|
43
|
+
"README.md",
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "bun build.ts",
|
|
48
|
+
"typecheck": "bun tsc --noEmit",
|
|
49
|
+
"prepublishOnly": "bun run build"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"better-dx": "^0.2.37"
|
|
53
|
+
},
|
|
54
|
+
"sideEffects": false
|
|
55
|
+
}
|