spectrum-ts 0.9.0 → 1.0.1

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.
@@ -0,0 +1,129 @@
1
+ // src/utils/stream.ts
2
+ import { Repeater } from "@repeaterjs/repeater";
3
+ function stream(setup) {
4
+ const repeater = new Repeater(async (push, stop) => {
5
+ const emit = async (value) => {
6
+ try {
7
+ await push(value);
8
+ } catch (error) {
9
+ stop(error);
10
+ throw error;
11
+ }
12
+ };
13
+ const end = (error) => {
14
+ stop(error);
15
+ };
16
+ const cleanup = await setup(emit, end);
17
+ try {
18
+ await stop;
19
+ } finally {
20
+ await cleanup?.();
21
+ }
22
+ });
23
+ return Object.assign(repeater, {
24
+ close: async () => {
25
+ await repeater.return(void 0);
26
+ }
27
+ });
28
+ }
29
+ function mergeStreams(streams) {
30
+ return stream((emit, end) => {
31
+ if (streams.length === 0) {
32
+ end();
33
+ return;
34
+ }
35
+ let openStreams = streams.length;
36
+ const workers = streams.map(async (source) => {
37
+ try {
38
+ for await (const value of source) {
39
+ await emit(value);
40
+ }
41
+ } catch (error) {
42
+ end(error);
43
+ } finally {
44
+ openStreams -= 1;
45
+ if (openStreams === 0) {
46
+ end();
47
+ }
48
+ }
49
+ });
50
+ return async () => {
51
+ await Promise.allSettled(streams.map((source) => source.close()));
52
+ await Promise.allSettled(workers);
53
+ };
54
+ });
55
+ }
56
+
57
+ // src/utils/cloud.ts
58
+ var SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum.photon.codes"}`;
59
+ var SpectrumCloudError = class extends Error {
60
+ status;
61
+ code;
62
+ constructor(status, code, message) {
63
+ super(message);
64
+ this.name = "SpectrumCloudError";
65
+ this.status = status;
66
+ this.code = code;
67
+ }
68
+ };
69
+ var request = async (path, init) => {
70
+ const response = await fetch(`${SPECTRUM_CLOUD_URL}${path}`, init);
71
+ if (!response.ok) {
72
+ const body = await response.text().catch(() => "");
73
+ try {
74
+ const parsed = JSON.parse(body);
75
+ throw new SpectrumCloudError(
76
+ response.status,
77
+ parsed.code,
78
+ parsed.message
79
+ );
80
+ } catch (error) {
81
+ if (error instanceof SpectrumCloudError) {
82
+ throw error;
83
+ }
84
+ throw new SpectrumCloudError(
85
+ response.status,
86
+ "UNKNOWN",
87
+ body || response.statusText
88
+ );
89
+ }
90
+ }
91
+ const json = await response.json();
92
+ if (!json.succeed) {
93
+ throw new SpectrumCloudError(
94
+ response.status,
95
+ "UNKNOWN",
96
+ "Server returned succeed=false"
97
+ );
98
+ }
99
+ return json.data;
100
+ };
101
+ var basicAuth = (projectId, projectSecret) => `Basic ${btoa(`${projectId}:${projectSecret}`)}`;
102
+ var cloud = {
103
+ getSubscription: (projectId) => request(`/projects/${projectId}/billing/subscription`),
104
+ issueImessageTokens: (projectId, projectSecret) => request(`/projects/${projectId}/imessage/tokens`, {
105
+ method: "POST",
106
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
107
+ }),
108
+ getImessageInfo: (projectId) => request(`/projects/${projectId}/imessage/`),
109
+ issueWhatsappBusinessTokens: (projectId, projectSecret) => request(`/projects/${projectId}/whatsapp-business/tokens`, {
110
+ method: "POST",
111
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
112
+ }),
113
+ getPlatforms: (projectId) => request(`/projects/${projectId}/platforms/`),
114
+ togglePlatform: (projectId, projectSecret, platform, enabled) => request(`/projects/${projectId}/platforms/`, {
115
+ method: "PATCH",
116
+ headers: {
117
+ Authorization: basicAuth(projectId, projectSecret),
118
+ "Content-Type": "application/json"
119
+ },
120
+ body: JSON.stringify({ platform, enabled })
121
+ })
122
+ };
123
+
124
+ export {
125
+ stream,
126
+ mergeStreams,
127
+ SpectrumCloudError,
128
+ cloud
129
+ };
@@ -0,0 +1,117 @@
1
+ import {
2
+ bufferToStream,
3
+ readSchema,
4
+ streamSchema
5
+ } from "./chunk-XMAI2AAN.js";
6
+
7
+ // src/content/voice.ts
8
+ import { createReadStream } from "fs";
9
+ import { readFile, stat } from "fs/promises";
10
+ import { basename } from "path";
11
+ import { Readable } from "stream";
12
+ import { lookup as lookupMimeType } from "mime-types";
13
+ import z from "zod";
14
+ var AUDIO_MIME_PATTERN = /^audio\//i;
15
+ var audioMimeSchema = z.string().nonempty().regex(AUDIO_MIME_PATTERN, "voice content requires an audio/* MIME type");
16
+ var voiceSchema = z.object({
17
+ type: z.literal("voice"),
18
+ name: z.string().nonempty().optional(),
19
+ mimeType: audioMimeSchema,
20
+ duration: z.number().nonnegative().optional(),
21
+ size: z.number().int().nonnegative().optional(),
22
+ read: readSchema,
23
+ stream: streamSchema
24
+ });
25
+ var resolveVoiceName = (input, name) => {
26
+ if (name) {
27
+ return name;
28
+ }
29
+ if (typeof input === "string") {
30
+ return basename(input);
31
+ }
32
+ return void 0;
33
+ };
34
+ var resolveVoiceMimeType = (name, mimeType) => {
35
+ if (mimeType) {
36
+ if (!AUDIO_MIME_PATTERN.test(mimeType)) {
37
+ throw new Error(
38
+ `voice content requires an audio/* MIME type, got "${mimeType}".`
39
+ );
40
+ }
41
+ return mimeType;
42
+ }
43
+ if (name) {
44
+ const resolved = lookupMimeType(name);
45
+ if (resolved && AUDIO_MIME_PATTERN.test(resolved)) {
46
+ return resolved;
47
+ }
48
+ if (resolved) {
49
+ throw new Error(
50
+ `Resolved non-audio MIME type "${resolved}" from name "${name}". Pass options.mimeType explicitly with an audio/* type.`
51
+ );
52
+ }
53
+ }
54
+ throw new Error(
55
+ "Unable to resolve MIME type for voice content. Pass options.mimeType explicitly."
56
+ );
57
+ };
58
+ var asVoice = (input) => {
59
+ let cached;
60
+ const read = () => {
61
+ cached ??= input.read().catch((err) => {
62
+ cached = void 0;
63
+ throw err;
64
+ });
65
+ return cached;
66
+ };
67
+ const stream = input.stream ?? (async () => bufferToStream(await read()));
68
+ return voiceSchema.parse({
69
+ type: "voice",
70
+ name: input.name,
71
+ mimeType: input.mimeType,
72
+ duration: input.duration,
73
+ size: input.size,
74
+ read,
75
+ stream
76
+ });
77
+ };
78
+ function voice(input, options) {
79
+ return {
80
+ build: async () => {
81
+ const name = resolveVoiceName(input, options?.name);
82
+ const mimeHint = typeof input === "string" ? basename(input) : name;
83
+ const mimeType = resolveVoiceMimeType(mimeHint, options?.mimeType);
84
+ if (typeof input === "string") {
85
+ const stats = await stat(input);
86
+ if (!stats.isFile()) {
87
+ throw new Error(
88
+ `voice content path "${input}" is not a regular file.`
89
+ );
90
+ }
91
+ return asVoice({
92
+ name,
93
+ mimeType,
94
+ duration: options?.duration,
95
+ size: stats.size,
96
+ read: () => readFile(input),
97
+ stream: async () => Readable.toWeb(
98
+ createReadStream(input)
99
+ )
100
+ });
101
+ }
102
+ return asVoice({
103
+ name,
104
+ mimeType,
105
+ duration: options?.duration,
106
+ size: input.byteLength,
107
+ read: async () => input,
108
+ stream: async () => bufferToStream(input)
109
+ });
110
+ }
111
+ };
112
+ }
113
+
114
+ export {
115
+ asVoice,
116
+ voice
117
+ };
@@ -1,11 +1,37 @@
1
1
  import {
2
2
  bufferToStream,
3
3
  readSchema,
4
+ resolveContents,
4
5
  streamSchema
5
- } from "./chunk-CZIWNTXP.js";
6
+ } from "./chunk-XMAI2AAN.js";
6
7
 
7
- // src/content/richlink.ts
8
+ // src/content/group.ts
8
9
  import z from "zod";
10
+ var isMessage = (v) => typeof v === "object" && v !== null && "id" in v && "content" in v;
11
+ var groupSchema = z.object({
12
+ type: z.literal("group"),
13
+ items: z.array(z.custom(isMessage)).min(2)
14
+ });
15
+ var asGroup = (input) => groupSchema.parse({ type: "group", items: input.items });
16
+ var stubOutboundMessage = (content) => ({ id: "", content });
17
+ function group(...items) {
18
+ return {
19
+ build: async () => {
20
+ const resolved = await resolveContents(items);
21
+ const members = [];
22
+ for (const item of resolved) {
23
+ if (item.type === "group" || item.type === "reaction") {
24
+ throw new Error(`group() cannot contain "${item.type}" items`);
25
+ }
26
+ members.push(stubOutboundMessage(item));
27
+ }
28
+ return asGroup({ items: members });
29
+ }
30
+ };
31
+ }
32
+
33
+ // src/content/richlink.ts
34
+ import z2 from "zod";
9
35
 
10
36
  // src/utils/link-metadata.ts
11
37
  import ogs from "open-graph-scraper";
@@ -76,22 +102,22 @@ var fetchImage = async (url) => {
76
102
  };
77
103
 
78
104
  // src/content/richlink.ts
79
- var richlinkCoverSchema = z.object({
80
- mimeType: z.string().min(1).optional(),
105
+ var richlinkCoverSchema = z2.object({
106
+ mimeType: z2.string().min(1).optional(),
81
107
  read: readSchema,
82
108
  stream: streamSchema
83
109
  });
84
- var optionalStringAccessor = z.function({
110
+ var optionalStringAccessor = z2.function({
85
111
  input: [],
86
- output: z.promise(z.string().min(1).optional())
112
+ output: z2.promise(z2.string().min(1).optional())
87
113
  });
88
- var coverAccessor = z.function({
114
+ var coverAccessor = z2.function({
89
115
  input: [],
90
- output: z.promise(richlinkCoverSchema.optional())
116
+ output: z2.promise(richlinkCoverSchema.optional())
91
117
  });
92
- var richlinkSchema = z.object({
93
- type: z.literal("richlink"),
94
- url: z.url(),
118
+ var richlinkSchema = z2.object({
119
+ type: z2.literal("richlink"),
120
+ url: z2.url(),
95
121
  title: optionalStringAccessor,
96
122
  summary: optionalStringAccessor,
97
123
  cover: coverAccessor
@@ -136,6 +162,8 @@ function richlink(url) {
136
162
  }
137
163
 
138
164
  export {
165
+ asGroup,
166
+ group,
139
167
  asRichlink,
140
168
  richlink
141
169
  };