tutticode 0.1.0 → 0.2.0
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/README.md +83 -11
- package/README.zh-CN.md +69 -9
- package/dist/skills/tutticode/SKILL.md +14 -3
- package/dist/skills/tutticode/references/agent-loop.md +43 -7
- package/dist/skills/tutticode/references/devices/sampler.md +1 -1
- package/dist/skills/tutticode/references/devices.md +1 -1
- package/dist/skills/tutticode/references/for-agents.md +6 -3
- package/dist/skills/tutticode/references/functions.md +1 -1
- package/dist/skills/tutticode/references/melodies-with-do-re-mi.md +59 -0
- package/dist/skills/tutticode-sound-for-media/SKILL.md +24 -10
- package/dist/tutticode.cjs +3123 -352
- package/package.json +1 -1
package/dist/tutticode.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* tutticode v0.
|
|
2
|
+
/* tutticode v0.2.0 | https://tuttidaw.com */
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
@@ -2310,7 +2310,7 @@ var require_websocket = __commonJS({
|
|
|
2310
2310
|
var http = require("http");
|
|
2311
2311
|
var net = require("net");
|
|
2312
2312
|
var tls = require("tls");
|
|
2313
|
-
var { randomBytes:
|
|
2313
|
+
var { randomBytes: randomBytes4, createHash: createHash3 } = require("crypto");
|
|
2314
2314
|
var { Duplex, Readable } = require("stream");
|
|
2315
2315
|
var { URL: URL2 } = require("url");
|
|
2316
2316
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2865,8 +2865,8 @@ var require_websocket = __commonJS({
|
|
|
2865
2865
|
}
|
|
2866
2866
|
}
|
|
2867
2867
|
const defaultPort = isSecure ? 443 : 80;
|
|
2868
|
-
const key =
|
|
2869
|
-
const
|
|
2868
|
+
const key = randomBytes4(16).toString("base64");
|
|
2869
|
+
const request2 = isSecure ? https.request : http.request;
|
|
2870
2870
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2871
2871
|
let perMessageDeflate;
|
|
2872
2872
|
opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
|
|
@@ -2944,12 +2944,12 @@ var require_websocket = __commonJS({
|
|
|
2944
2944
|
if (opts.auth && !options.headers.authorization) {
|
|
2945
2945
|
options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
|
|
2946
2946
|
}
|
|
2947
|
-
req = websocket._req =
|
|
2947
|
+
req = websocket._req = request2(opts);
|
|
2948
2948
|
if (websocket._redirects) {
|
|
2949
2949
|
websocket.emit("redirect", websocket.url, req);
|
|
2950
2950
|
}
|
|
2951
2951
|
} else {
|
|
2952
|
-
req = websocket._req =
|
|
2952
|
+
req = websocket._req = request2(opts);
|
|
2953
2953
|
}
|
|
2954
2954
|
if (opts.timeout) {
|
|
2955
2955
|
req.on("timeout", () => {
|
|
@@ -2998,7 +2998,7 @@ var require_websocket = __commonJS({
|
|
|
2998
2998
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2999
2999
|
return;
|
|
3000
3000
|
}
|
|
3001
|
-
const digest =
|
|
3001
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3002
3002
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
3003
3003
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
3004
3004
|
return;
|
|
@@ -3387,7 +3387,7 @@ var require_websocket_server = __commonJS({
|
|
|
3387
3387
|
var EventEmitter4 = require("events");
|
|
3388
3388
|
var http = require("http");
|
|
3389
3389
|
var { Duplex } = require("stream");
|
|
3390
|
-
var { createHash:
|
|
3390
|
+
var { createHash: createHash3 } = require("crypto");
|
|
3391
3391
|
var extension2 = require_extension();
|
|
3392
3392
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3393
3393
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3702,7 +3702,7 @@ var require_websocket_server = __commonJS({
|
|
|
3702
3702
|
}
|
|
3703
3703
|
if (this._state > RUNNING)
|
|
3704
3704
|
return abortHandshake(socket, 503);
|
|
3705
|
-
const digest =
|
|
3705
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3706
3706
|
const headers = [
|
|
3707
3707
|
"HTTP/1.1 101 Switching Protocols",
|
|
3708
3708
|
"Upgrade: websocket",
|
|
@@ -3791,12 +3791,703 @@ var require_websocket_server = __commonJS({
|
|
|
3791
3791
|
});
|
|
3792
3792
|
|
|
3793
3793
|
// src/bin.ts
|
|
3794
|
-
var
|
|
3794
|
+
var import_node_path12 = require("node:path");
|
|
3795
3795
|
|
|
3796
3796
|
// src/cli.ts
|
|
3797
|
-
var
|
|
3797
|
+
var import_node_fs11 = require("node:fs");
|
|
3798
3798
|
var import_node_readline2 = require("node:readline");
|
|
3799
3799
|
|
|
3800
|
+
// ../../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
|
|
3801
|
+
var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
|
|
3802
|
+
QrCodeDataType2[QrCodeDataType2["Border"] = -1] = "Border";
|
|
3803
|
+
QrCodeDataType2[QrCodeDataType2["Data"] = 0] = "Data";
|
|
3804
|
+
QrCodeDataType2[QrCodeDataType2["Function"] = 1] = "Function";
|
|
3805
|
+
QrCodeDataType2[QrCodeDataType2["Position"] = 2] = "Position";
|
|
3806
|
+
QrCodeDataType2[QrCodeDataType2["Timing"] = 3] = "Timing";
|
|
3807
|
+
QrCodeDataType2[QrCodeDataType2["Alignment"] = 4] = "Alignment";
|
|
3808
|
+
return QrCodeDataType2;
|
|
3809
|
+
})(QrCodeDataType || {});
|
|
3810
|
+
var LOW = [0, 1];
|
|
3811
|
+
var MEDIUM = [1, 0];
|
|
3812
|
+
var QUARTILE = [2, 3];
|
|
3813
|
+
var HIGH = [3, 2];
|
|
3814
|
+
var EccMap = {
|
|
3815
|
+
L: LOW,
|
|
3816
|
+
M: MEDIUM,
|
|
3817
|
+
Q: QUARTILE,
|
|
3818
|
+
H: HIGH
|
|
3819
|
+
};
|
|
3820
|
+
var NUMERIC_REGEX = /^\d*$/;
|
|
3821
|
+
var ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:-]*$/;
|
|
3822
|
+
var ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
3823
|
+
var MIN_VERSION = 1;
|
|
3824
|
+
var MAX_VERSION = 40;
|
|
3825
|
+
var PENALTY_N1 = 3;
|
|
3826
|
+
var PENALTY_N2 = 3;
|
|
3827
|
+
var PENALTY_N3 = 40;
|
|
3828
|
+
var PENALTY_N4 = 10;
|
|
3829
|
+
var ECC_CODEWORDS_PER_BLOCK = [
|
|
3830
|
+
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
|
3831
|
+
// 0, 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 Error correction level
|
|
3832
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
3833
|
+
// Low
|
|
3834
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
3835
|
+
// Medium
|
|
3836
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
3837
|
+
// Quartile
|
|
3838
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
|
|
3839
|
+
// High
|
|
3840
|
+
];
|
|
3841
|
+
var NUM_ERROR_CORRECTION_BLOCKS = [
|
|
3842
|
+
// Version: (note that index 0 is for padding, and is set to an illegal value)
|
|
3843
|
+
// 0, 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 Error correction level
|
|
3844
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
3845
|
+
// Low
|
|
3846
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
3847
|
+
// Medium
|
|
3848
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
3849
|
+
// Quartile
|
|
3850
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]
|
|
3851
|
+
// High
|
|
3852
|
+
];
|
|
3853
|
+
var QrCode = class {
|
|
3854
|
+
/* -- Constructor (low level) and fields -- */
|
|
3855
|
+
// Creates a new QR Code with the given version number,
|
|
3856
|
+
// error correction level, data codeword bytes, and mask number.
|
|
3857
|
+
// This is a low-level API that most users should not use directly.
|
|
3858
|
+
// A mid-level API is the encodeSegments() function.
|
|
3859
|
+
constructor(version, ecc, dataCodewords, msk) {
|
|
3860
|
+
this.version = version;
|
|
3861
|
+
this.ecc = ecc;
|
|
3862
|
+
if (version < MIN_VERSION || version > MAX_VERSION)
|
|
3863
|
+
throw new RangeError("Version value out of range");
|
|
3864
|
+
if (msk < -1 || msk > 7)
|
|
3865
|
+
throw new RangeError("Mask value out of range");
|
|
3866
|
+
this.size = version * 4 + 17;
|
|
3867
|
+
const row = Array.from({ length: this.size }).fill(false);
|
|
3868
|
+
for (let i = 0; i < this.size; i++) {
|
|
3869
|
+
this.modules.push(row.slice());
|
|
3870
|
+
this.types.push(row.map(() => 0));
|
|
3871
|
+
}
|
|
3872
|
+
this.drawFunctionPatterns();
|
|
3873
|
+
const allCodewords = this.addEccAndInterleave(dataCodewords);
|
|
3874
|
+
this.drawCodewords(allCodewords);
|
|
3875
|
+
if (msk === -1) {
|
|
3876
|
+
let minPenalty = 1e9;
|
|
3877
|
+
for (let i = 0; i < 8; i++) {
|
|
3878
|
+
this.applyMask(i);
|
|
3879
|
+
this.drawFormatBits(i);
|
|
3880
|
+
const penalty = this.getPenaltyScore();
|
|
3881
|
+
if (penalty < minPenalty) {
|
|
3882
|
+
msk = i;
|
|
3883
|
+
minPenalty = penalty;
|
|
3884
|
+
}
|
|
3885
|
+
this.applyMask(i);
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
this.mask = msk;
|
|
3889
|
+
this.applyMask(msk);
|
|
3890
|
+
this.drawFormatBits(msk);
|
|
3891
|
+
}
|
|
3892
|
+
/* -- Fields -- */
|
|
3893
|
+
// The width and height of this QR Code, measured in modules, between
|
|
3894
|
+
// 21 and 177 (inclusive). This is equal to version * 4 + 17.
|
|
3895
|
+
size;
|
|
3896
|
+
// The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
|
|
3897
|
+
// Even if a QR Code is created with automatic masking requested (mask = -1),
|
|
3898
|
+
// the resulting object still has a mask value between 0 and 7.
|
|
3899
|
+
mask;
|
|
3900
|
+
// The modules of this QR Code (false = light, true = dark).
|
|
3901
|
+
// Immutable after constructor finishes. Accessed through getModule().
|
|
3902
|
+
modules = [];
|
|
3903
|
+
types = [];
|
|
3904
|
+
/* -- Accessor methods -- */
|
|
3905
|
+
// Returns the color of the module (pixel) at the given coordinates, which is false
|
|
3906
|
+
// for light or true for dark. The top left corner has the coordinates (x=0, y=0).
|
|
3907
|
+
// If the given coordinates are out of bounds, then false (light) is returned.
|
|
3908
|
+
getModule(x, y) {
|
|
3909
|
+
return x >= 0 && x < this.size && y >= 0 && y < this.size && this.modules[y][x];
|
|
3910
|
+
}
|
|
3911
|
+
/* -- Private helper methods for constructor: Drawing function modules -- */
|
|
3912
|
+
// Reads this object's version field, and draws and marks all function modules.
|
|
3913
|
+
drawFunctionPatterns() {
|
|
3914
|
+
for (let i = 0; i < this.size; i++) {
|
|
3915
|
+
this.setFunctionModule(6, i, i % 2 === 0, QrCodeDataType.Timing);
|
|
3916
|
+
this.setFunctionModule(i, 6, i % 2 === 0, QrCodeDataType.Timing);
|
|
3917
|
+
}
|
|
3918
|
+
this.drawFinderPattern(3, 3);
|
|
3919
|
+
this.drawFinderPattern(this.size - 4, 3);
|
|
3920
|
+
this.drawFinderPattern(3, this.size - 4);
|
|
3921
|
+
const alignPatPos = this.getAlignmentPatternPositions();
|
|
3922
|
+
const numAlign = alignPatPos.length;
|
|
3923
|
+
for (let i = 0; i < numAlign; i++) {
|
|
3924
|
+
for (let j = 0; j < numAlign; j++) {
|
|
3925
|
+
if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
|
|
3926
|
+
this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
this.drawFormatBits(0);
|
|
3930
|
+
this.drawVersion();
|
|
3931
|
+
}
|
|
3932
|
+
// Draws two copies of the format bits (with its own error correction code)
|
|
3933
|
+
// based on the given mask and this object's error correction level field.
|
|
3934
|
+
drawFormatBits(mask) {
|
|
3935
|
+
const data = this.ecc[1] << 3 | mask;
|
|
3936
|
+
let rem = data;
|
|
3937
|
+
for (let i = 0; i < 10; i++)
|
|
3938
|
+
rem = rem << 1 ^ (rem >>> 9) * 1335;
|
|
3939
|
+
const bits = (data << 10 | rem) ^ 21522;
|
|
3940
|
+
for (let i = 0; i <= 5; i++)
|
|
3941
|
+
this.setFunctionModule(8, i, getBit(bits, i));
|
|
3942
|
+
this.setFunctionModule(8, 7, getBit(bits, 6));
|
|
3943
|
+
this.setFunctionModule(8, 8, getBit(bits, 7));
|
|
3944
|
+
this.setFunctionModule(7, 8, getBit(bits, 8));
|
|
3945
|
+
for (let i = 9; i < 15; i++)
|
|
3946
|
+
this.setFunctionModule(14 - i, 8, getBit(bits, i));
|
|
3947
|
+
for (let i = 0; i < 8; i++)
|
|
3948
|
+
this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
|
|
3949
|
+
for (let i = 8; i < 15; i++)
|
|
3950
|
+
this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
|
|
3951
|
+
this.setFunctionModule(8, this.size - 8, true);
|
|
3952
|
+
}
|
|
3953
|
+
// Draws two copies of the version bits (with its own error correction code),
|
|
3954
|
+
// based on this object's version field, iff 7 <= version <= 40.
|
|
3955
|
+
drawVersion() {
|
|
3956
|
+
if (this.version < 7)
|
|
3957
|
+
return;
|
|
3958
|
+
let rem = this.version;
|
|
3959
|
+
for (let i = 0; i < 12; i++)
|
|
3960
|
+
rem = rem << 1 ^ (rem >>> 11) * 7973;
|
|
3961
|
+
const bits = this.version << 12 | rem;
|
|
3962
|
+
for (let i = 0; i < 18; i++) {
|
|
3963
|
+
const color = getBit(bits, i);
|
|
3964
|
+
const a = this.size - 11 + i % 3;
|
|
3965
|
+
const b = Math.floor(i / 3);
|
|
3966
|
+
this.setFunctionModule(a, b, color);
|
|
3967
|
+
this.setFunctionModule(b, a, color);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
// Draws a 9*9 finder pattern including the border separator,
|
|
3971
|
+
// with the center module at (x, y). Modules can be out of bounds.
|
|
3972
|
+
drawFinderPattern(x, y) {
|
|
3973
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
3974
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
3975
|
+
const dist = Math.max(Math.abs(dx), Math.abs(dy));
|
|
3976
|
+
const xx = x + dx;
|
|
3977
|
+
const yy = y + dy;
|
|
3978
|
+
if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size)
|
|
3979
|
+
this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4, QrCodeDataType.Position);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
// Draws a 5*5 alignment pattern, with the center module
|
|
3984
|
+
// at (x, y). All modules must be in bounds.
|
|
3985
|
+
drawAlignmentPattern(x, y) {
|
|
3986
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
3987
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
3988
|
+
this.setFunctionModule(
|
|
3989
|
+
x + dx,
|
|
3990
|
+
y + dy,
|
|
3991
|
+
Math.max(Math.abs(dx), Math.abs(dy)) !== 1,
|
|
3992
|
+
QrCodeDataType.Alignment
|
|
3993
|
+
);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
// Sets the color of a module and marks it as a function module.
|
|
3998
|
+
// Only used by the constructor. Coordinates must be in bounds.
|
|
3999
|
+
setFunctionModule(x, y, isDark, type = QrCodeDataType.Function) {
|
|
4000
|
+
this.modules[y][x] = isDark;
|
|
4001
|
+
this.types[y][x] = type;
|
|
4002
|
+
}
|
|
4003
|
+
/* -- Private helper methods for constructor: Codewords and masking -- */
|
|
4004
|
+
// Returns a new byte string representing the given data with the appropriate error correction
|
|
4005
|
+
// codewords appended to it, based on this object's version and error correction level.
|
|
4006
|
+
addEccAndInterleave(data) {
|
|
4007
|
+
const ver = this.version;
|
|
4008
|
+
const ecl = this.ecc;
|
|
4009
|
+
if (data.length !== getNumDataCodewords(ver, ecl))
|
|
4010
|
+
throw new RangeError("Invalid argument");
|
|
4011
|
+
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
|
|
4012
|
+
const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver];
|
|
4013
|
+
const rawCodewords = Math.floor(getNumRawDataModules(ver) / 8);
|
|
4014
|
+
const numShortBlocks = numBlocks - rawCodewords % numBlocks;
|
|
4015
|
+
const shortBlockLen = Math.floor(rawCodewords / numBlocks);
|
|
4016
|
+
const blocks = [];
|
|
4017
|
+
const rsDiv = reedSolomonComputeDivisor(blockEccLen);
|
|
4018
|
+
for (let i = 0, k = 0; i < numBlocks; i++) {
|
|
4019
|
+
const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
|
|
4020
|
+
k += dat.length;
|
|
4021
|
+
const ecc = reedSolomonComputeRemainder(dat, rsDiv);
|
|
4022
|
+
if (i < numShortBlocks)
|
|
4023
|
+
dat.push(0);
|
|
4024
|
+
blocks.push(dat.concat(ecc));
|
|
4025
|
+
}
|
|
4026
|
+
const result = [];
|
|
4027
|
+
for (let i = 0; i < blocks[0].length; i++) {
|
|
4028
|
+
blocks.forEach((block, j) => {
|
|
4029
|
+
if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
|
|
4030
|
+
result.push(block[i]);
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
return result;
|
|
4034
|
+
}
|
|
4035
|
+
// Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
|
|
4036
|
+
// data area of this QR Code. Function modules need to be marked off before this is called.
|
|
4037
|
+
drawCodewords(data) {
|
|
4038
|
+
if (data.length !== Math.floor(getNumRawDataModules(this.version) / 8))
|
|
4039
|
+
throw new RangeError("Invalid argument");
|
|
4040
|
+
let i = 0;
|
|
4041
|
+
for (let right = this.size - 1; right >= 1; right -= 2) {
|
|
4042
|
+
if (right === 6)
|
|
4043
|
+
right = 5;
|
|
4044
|
+
for (let vert = 0; vert < this.size; vert++) {
|
|
4045
|
+
for (let j = 0; j < 2; j++) {
|
|
4046
|
+
const x = right - j;
|
|
4047
|
+
const upward = (right + 1 & 2) === 0;
|
|
4048
|
+
const y = upward ? this.size - 1 - vert : vert;
|
|
4049
|
+
if (!this.types[y][x] && i < data.length * 8) {
|
|
4050
|
+
this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
|
|
4051
|
+
i++;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
// XORs the codeword modules in this QR Code with the given mask pattern.
|
|
4058
|
+
// The function modules must be marked and the codeword bits must be drawn
|
|
4059
|
+
// before masking. Due to the arithmetic of XOR, calling applyMask() with
|
|
4060
|
+
// the same mask value a second time will undo the mask. A final well-formed
|
|
4061
|
+
// QR Code needs exactly one (not zero, two, etc.) mask applied.
|
|
4062
|
+
applyMask(mask) {
|
|
4063
|
+
if (mask < 0 || mask > 7)
|
|
4064
|
+
throw new RangeError("Mask value out of range");
|
|
4065
|
+
for (let y = 0; y < this.size; y++) {
|
|
4066
|
+
for (let x = 0; x < this.size; x++) {
|
|
4067
|
+
let invert;
|
|
4068
|
+
switch (mask) {
|
|
4069
|
+
case 0:
|
|
4070
|
+
invert = (x + y) % 2 === 0;
|
|
4071
|
+
break;
|
|
4072
|
+
case 1:
|
|
4073
|
+
invert = y % 2 === 0;
|
|
4074
|
+
break;
|
|
4075
|
+
case 2:
|
|
4076
|
+
invert = x % 3 === 0;
|
|
4077
|
+
break;
|
|
4078
|
+
case 3:
|
|
4079
|
+
invert = (x + y) % 3 === 0;
|
|
4080
|
+
break;
|
|
4081
|
+
case 4:
|
|
4082
|
+
invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
|
|
4083
|
+
break;
|
|
4084
|
+
case 5:
|
|
4085
|
+
invert = x * y % 2 + x * y % 3 === 0;
|
|
4086
|
+
break;
|
|
4087
|
+
case 6:
|
|
4088
|
+
invert = (x * y % 2 + x * y % 3) % 2 === 0;
|
|
4089
|
+
break;
|
|
4090
|
+
case 7:
|
|
4091
|
+
invert = ((x + y) % 2 + x * y % 3) % 2 === 0;
|
|
4092
|
+
break;
|
|
4093
|
+
default:
|
|
4094
|
+
throw new Error("Unreachable");
|
|
4095
|
+
}
|
|
4096
|
+
if (!this.types[y][x] && invert)
|
|
4097
|
+
this.modules[y][x] = !this.modules[y][x];
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
// Calculates and returns the penalty score based on state of this QR Code's current modules.
|
|
4102
|
+
// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
|
|
4103
|
+
getPenaltyScore() {
|
|
4104
|
+
let result = 0;
|
|
4105
|
+
for (let y = 0; y < this.size; y++) {
|
|
4106
|
+
let runColor = false;
|
|
4107
|
+
let runX = 0;
|
|
4108
|
+
const runHistory = [0, 0, 0, 0, 0, 0, 0];
|
|
4109
|
+
for (let x = 0; x < this.size; x++) {
|
|
4110
|
+
if (this.modules[y][x] === runColor) {
|
|
4111
|
+
runX++;
|
|
4112
|
+
if (runX === 5)
|
|
4113
|
+
result += PENALTY_N1;
|
|
4114
|
+
else if (runX > 5)
|
|
4115
|
+
result++;
|
|
4116
|
+
} else {
|
|
4117
|
+
this.finderPenaltyAddHistory(runX, runHistory);
|
|
4118
|
+
if (!runColor)
|
|
4119
|
+
result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
|
4120
|
+
runColor = this.modules[y][x];
|
|
4121
|
+
runX = 1;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3;
|
|
4125
|
+
}
|
|
4126
|
+
for (let x = 0; x < this.size; x++) {
|
|
4127
|
+
let runColor = false;
|
|
4128
|
+
let runY = 0;
|
|
4129
|
+
const runHistory = [0, 0, 0, 0, 0, 0, 0];
|
|
4130
|
+
for (let y = 0; y < this.size; y++) {
|
|
4131
|
+
if (this.modules[y][x] === runColor) {
|
|
4132
|
+
runY++;
|
|
4133
|
+
if (runY === 5)
|
|
4134
|
+
result += PENALTY_N1;
|
|
4135
|
+
else if (runY > 5)
|
|
4136
|
+
result++;
|
|
4137
|
+
} else {
|
|
4138
|
+
this.finderPenaltyAddHistory(runY, runHistory);
|
|
4139
|
+
if (!runColor)
|
|
4140
|
+
result += this.finderPenaltyCountPatterns(runHistory) * PENALTY_N3;
|
|
4141
|
+
runColor = this.modules[y][x];
|
|
4142
|
+
runY = 1;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
result += this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3;
|
|
4146
|
+
}
|
|
4147
|
+
for (let y = 0; y < this.size - 1; y++) {
|
|
4148
|
+
for (let x = 0; x < this.size - 1; x++) {
|
|
4149
|
+
const color = this.modules[y][x];
|
|
4150
|
+
if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
|
|
4151
|
+
result += PENALTY_N2;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
let dark = 0;
|
|
4156
|
+
for (const row of this.modules)
|
|
4157
|
+
dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
|
|
4158
|
+
const total = this.size * this.size;
|
|
4159
|
+
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
|
|
4160
|
+
result += k * PENALTY_N4;
|
|
4161
|
+
return result;
|
|
4162
|
+
}
|
|
4163
|
+
/* -- Private helper functions -- */
|
|
4164
|
+
// Returns an ascending list of positions of alignment patterns for this version number.
|
|
4165
|
+
// Each position is in the range [0,177), and are used on both the x and y axes.
|
|
4166
|
+
// This could be implemented as lookup table of 40 variable-length lists of integers.
|
|
4167
|
+
getAlignmentPatternPositions() {
|
|
4168
|
+
if (this.version === 1) {
|
|
4169
|
+
return [];
|
|
4170
|
+
} else {
|
|
4171
|
+
const numAlign = Math.floor(this.version / 7) + 2;
|
|
4172
|
+
const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
|
|
4173
|
+
const result = [6];
|
|
4174
|
+
for (let pos = this.size - 7; result.length < numAlign; pos -= step)
|
|
4175
|
+
result.splice(1, 0, pos);
|
|
4176
|
+
return result;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
// Can only be called immediately after a light run is added, and
|
|
4180
|
+
// returns either 0, 1, or 2. A helper function for getPenaltyScore().
|
|
4181
|
+
finderPenaltyCountPatterns(runHistory) {
|
|
4182
|
+
const n = runHistory[1];
|
|
4183
|
+
const core = n > 0 && runHistory[2] === n && runHistory[3] === n * 3 && runHistory[4] === n && runHistory[5] === n;
|
|
4184
|
+
return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
|
|
4185
|
+
}
|
|
4186
|
+
// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
|
|
4187
|
+
finderPenaltyTerminateAndCount(currentRunColor, currentRunLength, runHistory) {
|
|
4188
|
+
if (currentRunColor) {
|
|
4189
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
4190
|
+
currentRunLength = 0;
|
|
4191
|
+
}
|
|
4192
|
+
currentRunLength += this.size;
|
|
4193
|
+
this.finderPenaltyAddHistory(currentRunLength, runHistory);
|
|
4194
|
+
return this.finderPenaltyCountPatterns(runHistory);
|
|
4195
|
+
}
|
|
4196
|
+
// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
|
|
4197
|
+
finderPenaltyAddHistory(currentRunLength, runHistory) {
|
|
4198
|
+
if (runHistory[0] === 0)
|
|
4199
|
+
currentRunLength += this.size;
|
|
4200
|
+
runHistory.pop();
|
|
4201
|
+
runHistory.unshift(currentRunLength);
|
|
4202
|
+
}
|
|
4203
|
+
};
|
|
4204
|
+
function appendBits(val, len, bb) {
|
|
4205
|
+
if (len < 0 || len > 31 || val >>> len !== 0)
|
|
4206
|
+
throw new RangeError("Value out of range");
|
|
4207
|
+
for (let i = len - 1; i >= 0; i--)
|
|
4208
|
+
bb.push(val >>> i & 1);
|
|
4209
|
+
}
|
|
4210
|
+
function getBit(x, i) {
|
|
4211
|
+
return (x >>> i & 1) !== 0;
|
|
4212
|
+
}
|
|
4213
|
+
var QrSegment = class {
|
|
4214
|
+
// Creates a new QR Code segment with the given attributes and data.
|
|
4215
|
+
// The character count (numChars) must agree with the mode and the bit buffer length,
|
|
4216
|
+
// but the constraint isn't checked. The given bit buffer is cloned and stored.
|
|
4217
|
+
constructor(mode, numChars, bitData) {
|
|
4218
|
+
this.mode = mode;
|
|
4219
|
+
this.numChars = numChars;
|
|
4220
|
+
this.bitData = bitData;
|
|
4221
|
+
if (numChars < 0)
|
|
4222
|
+
throw new RangeError("Invalid argument");
|
|
4223
|
+
this.bitData = bitData.slice();
|
|
4224
|
+
}
|
|
4225
|
+
/* -- Methods -- */
|
|
4226
|
+
// Returns a new copy of the data bits of this segment.
|
|
4227
|
+
getData() {
|
|
4228
|
+
return this.bitData.slice();
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4231
|
+
var MODE_NUMERIC = [1, 10, 12, 14];
|
|
4232
|
+
var MODE_ALPHANUMERIC = [2, 9, 11, 13];
|
|
4233
|
+
var MODE_BYTE = [4, 8, 16, 16];
|
|
4234
|
+
function numCharCountBits(mode, ver) {
|
|
4235
|
+
return mode[Math.floor((ver + 7) / 17) + 1];
|
|
4236
|
+
}
|
|
4237
|
+
function makeBytes(data) {
|
|
4238
|
+
const bb = [];
|
|
4239
|
+
for (const b of data)
|
|
4240
|
+
appendBits(b, 8, bb);
|
|
4241
|
+
return new QrSegment(MODE_BYTE, data.length, bb);
|
|
4242
|
+
}
|
|
4243
|
+
function makeNumeric(digits) {
|
|
4244
|
+
if (!isNumeric(digits))
|
|
4245
|
+
throw new RangeError("String contains non-numeric characters");
|
|
4246
|
+
const bb = [];
|
|
4247
|
+
for (let i = 0; i < digits.length; ) {
|
|
4248
|
+
const n = Math.min(digits.length - i, 3);
|
|
4249
|
+
appendBits(Number.parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
|
|
4250
|
+
i += n;
|
|
4251
|
+
}
|
|
4252
|
+
return new QrSegment(MODE_NUMERIC, digits.length, bb);
|
|
4253
|
+
}
|
|
4254
|
+
function makeAlphanumeric(text) {
|
|
4255
|
+
if (!isAlphanumeric(text))
|
|
4256
|
+
throw new RangeError("String contains unencodable characters in alphanumeric mode");
|
|
4257
|
+
const bb = [];
|
|
4258
|
+
let i;
|
|
4259
|
+
for (i = 0; i + 2 <= text.length; i += 2) {
|
|
4260
|
+
let temp = ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
|
|
4261
|
+
temp += ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
|
|
4262
|
+
appendBits(temp, 11, bb);
|
|
4263
|
+
}
|
|
4264
|
+
if (i < text.length)
|
|
4265
|
+
appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
|
|
4266
|
+
return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
|
|
4267
|
+
}
|
|
4268
|
+
function makeSegments(text) {
|
|
4269
|
+
if (text === "")
|
|
4270
|
+
return [];
|
|
4271
|
+
else if (isNumeric(text))
|
|
4272
|
+
return [makeNumeric(text)];
|
|
4273
|
+
else if (isAlphanumeric(text))
|
|
4274
|
+
return [makeAlphanumeric(text)];
|
|
4275
|
+
else
|
|
4276
|
+
return [makeBytes(toUtf8ByteArray(text))];
|
|
4277
|
+
}
|
|
4278
|
+
function isNumeric(text) {
|
|
4279
|
+
return NUMERIC_REGEX.test(text);
|
|
4280
|
+
}
|
|
4281
|
+
function isAlphanumeric(text) {
|
|
4282
|
+
return ALPHANUMERIC_REGEX.test(text);
|
|
4283
|
+
}
|
|
4284
|
+
function getTotalBits(segs, version) {
|
|
4285
|
+
let result = 0;
|
|
4286
|
+
for (const seg of segs) {
|
|
4287
|
+
const ccbits = numCharCountBits(seg.mode, version);
|
|
4288
|
+
if (seg.numChars >= 1 << ccbits)
|
|
4289
|
+
return Number.POSITIVE_INFINITY;
|
|
4290
|
+
result += 4 + ccbits + seg.bitData.length;
|
|
4291
|
+
}
|
|
4292
|
+
return result;
|
|
4293
|
+
}
|
|
4294
|
+
function toUtf8ByteArray(str) {
|
|
4295
|
+
str = encodeURI(str);
|
|
4296
|
+
const result = [];
|
|
4297
|
+
for (let i = 0; i < str.length; i++) {
|
|
4298
|
+
if (str.charAt(i) !== "%") {
|
|
4299
|
+
result.push(str.charCodeAt(i));
|
|
4300
|
+
} else {
|
|
4301
|
+
result.push(Number.parseInt(str.substring(i + 1, i + 3), 16));
|
|
4302
|
+
i += 2;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
return result;
|
|
4306
|
+
}
|
|
4307
|
+
function getNumRawDataModules(ver) {
|
|
4308
|
+
if (ver < MIN_VERSION || ver > MAX_VERSION)
|
|
4309
|
+
throw new RangeError("Version number out of range");
|
|
4310
|
+
let result = (16 * ver + 128) * ver + 64;
|
|
4311
|
+
if (ver >= 2) {
|
|
4312
|
+
const numAlign = Math.floor(ver / 7) + 2;
|
|
4313
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
4314
|
+
if (ver >= 7)
|
|
4315
|
+
result -= 36;
|
|
4316
|
+
}
|
|
4317
|
+
return result;
|
|
4318
|
+
}
|
|
4319
|
+
function getNumDataCodewords(ver, ecl) {
|
|
4320
|
+
return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver];
|
|
4321
|
+
}
|
|
4322
|
+
function reedSolomonComputeDivisor(degree) {
|
|
4323
|
+
if (degree < 1 || degree > 255)
|
|
4324
|
+
throw new RangeError("Degree out of range");
|
|
4325
|
+
const result = [];
|
|
4326
|
+
for (let i = 0; i < degree - 1; i++)
|
|
4327
|
+
result.push(0);
|
|
4328
|
+
result.push(1);
|
|
4329
|
+
let root = 1;
|
|
4330
|
+
for (let i = 0; i < degree; i++) {
|
|
4331
|
+
for (let j = 0; j < result.length; j++) {
|
|
4332
|
+
result[j] = reedSolomonMultiply(result[j], root);
|
|
4333
|
+
if (j + 1 < result.length)
|
|
4334
|
+
result[j] ^= result[j + 1];
|
|
4335
|
+
}
|
|
4336
|
+
root = reedSolomonMultiply(root, 2);
|
|
4337
|
+
}
|
|
4338
|
+
return result;
|
|
4339
|
+
}
|
|
4340
|
+
function reedSolomonComputeRemainder(data, divisor) {
|
|
4341
|
+
const result = divisor.map((_) => 0);
|
|
4342
|
+
for (const b of data) {
|
|
4343
|
+
const factor = b ^ result.shift();
|
|
4344
|
+
result.push(0);
|
|
4345
|
+
divisor.forEach((coef, i) => result[i] ^= reedSolomonMultiply(coef, factor));
|
|
4346
|
+
}
|
|
4347
|
+
return result;
|
|
4348
|
+
}
|
|
4349
|
+
function reedSolomonMultiply(x, y) {
|
|
4350
|
+
if (x >>> 8 !== 0 || y >>> 8 !== 0)
|
|
4351
|
+
throw new RangeError("Byte out of range");
|
|
4352
|
+
let z = 0;
|
|
4353
|
+
for (let i = 7; i >= 0; i--) {
|
|
4354
|
+
z = z << 1 ^ (z >>> 7) * 285;
|
|
4355
|
+
z ^= (y >>> i & 1) * x;
|
|
4356
|
+
}
|
|
4357
|
+
return z;
|
|
4358
|
+
}
|
|
4359
|
+
function encodeSegments(segs, ecl, minVersion = 1, maxVersion = 40, mask = -1, boostEcl = true) {
|
|
4360
|
+
if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) {
|
|
4361
|
+
throw new RangeError("Invalid value");
|
|
4362
|
+
}
|
|
4363
|
+
let version;
|
|
4364
|
+
let dataUsedBits;
|
|
4365
|
+
for (version = minVersion; ; version++) {
|
|
4366
|
+
const dataCapacityBits2 = getNumDataCodewords(version, ecl) * 8;
|
|
4367
|
+
const usedBits = getTotalBits(segs, version);
|
|
4368
|
+
if (usedBits <= dataCapacityBits2) {
|
|
4369
|
+
dataUsedBits = usedBits;
|
|
4370
|
+
break;
|
|
4371
|
+
}
|
|
4372
|
+
if (version >= maxVersion)
|
|
4373
|
+
throw new RangeError("Data too long");
|
|
4374
|
+
}
|
|
4375
|
+
for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
|
|
4376
|
+
if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8)
|
|
4377
|
+
ecl = newEcl;
|
|
4378
|
+
}
|
|
4379
|
+
const bb = [];
|
|
4380
|
+
for (const seg of segs) {
|
|
4381
|
+
appendBits(seg.mode[0], 4, bb);
|
|
4382
|
+
appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
|
|
4383
|
+
for (const b of seg.getData())
|
|
4384
|
+
bb.push(b);
|
|
4385
|
+
}
|
|
4386
|
+
const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
|
|
4387
|
+
appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
|
|
4388
|
+
appendBits(0, (8 - bb.length % 8) % 8, bb);
|
|
4389
|
+
for (let padByte = 236; bb.length < dataCapacityBits; padByte ^= 236 ^ 17)
|
|
4390
|
+
appendBits(padByte, 8, bb);
|
|
4391
|
+
const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
|
|
4392
|
+
bb.forEach((b, i) => dataCodewords[i >>> 3] |= b << 7 - (i & 7));
|
|
4393
|
+
return new QrCode(version, ecl, dataCodewords, mask);
|
|
4394
|
+
}
|
|
4395
|
+
function encode(data, options) {
|
|
4396
|
+
const {
|
|
4397
|
+
ecc = "L",
|
|
4398
|
+
boostEcc = false,
|
|
4399
|
+
minVersion = 1,
|
|
4400
|
+
maxVersion = 40,
|
|
4401
|
+
maskPattern = -1,
|
|
4402
|
+
border = 1
|
|
4403
|
+
} = options || {};
|
|
4404
|
+
const segment = typeof data === "string" ? makeSegments(data) : Array.isArray(data) ? [makeBytes(data)] : void 0;
|
|
4405
|
+
if (!segment)
|
|
4406
|
+
throw new Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);
|
|
4407
|
+
const qr = encodeSegments(
|
|
4408
|
+
segment,
|
|
4409
|
+
EccMap[ecc],
|
|
4410
|
+
minVersion,
|
|
4411
|
+
maxVersion,
|
|
4412
|
+
maskPattern,
|
|
4413
|
+
boostEcc
|
|
4414
|
+
);
|
|
4415
|
+
const result = addBorder({
|
|
4416
|
+
version: qr.version,
|
|
4417
|
+
maskPattern: qr.mask,
|
|
4418
|
+
size: qr.size,
|
|
4419
|
+
data: qr.modules,
|
|
4420
|
+
types: qr.types
|
|
4421
|
+
}, border);
|
|
4422
|
+
if (options?.invert)
|
|
4423
|
+
result.data = result.data.map((row) => row.map((mod) => !mod));
|
|
4424
|
+
options?.onEncoded?.(result);
|
|
4425
|
+
return result;
|
|
4426
|
+
}
|
|
4427
|
+
function addBorder(input, border = 1) {
|
|
4428
|
+
if (!border)
|
|
4429
|
+
return input;
|
|
4430
|
+
const { size } = input;
|
|
4431
|
+
const newSize = size + border * 2;
|
|
4432
|
+
input.size = newSize;
|
|
4433
|
+
input.data.forEach((row) => {
|
|
4434
|
+
for (let i = 0; i < border; i++) {
|
|
4435
|
+
row.unshift(false);
|
|
4436
|
+
row.push(false);
|
|
4437
|
+
}
|
|
4438
|
+
});
|
|
4439
|
+
for (let i = 0; i < border; i++) {
|
|
4440
|
+
input.data.unshift(Array.from({ length: newSize }, (_) => false));
|
|
4441
|
+
input.data.push(Array.from({ length: newSize }, (_) => false));
|
|
4442
|
+
}
|
|
4443
|
+
const b = QrCodeDataType.Border;
|
|
4444
|
+
input.types.forEach((row) => {
|
|
4445
|
+
for (let i = 0; i < border; i++) {
|
|
4446
|
+
row.unshift(b);
|
|
4447
|
+
row.push(b);
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
for (let i = 0; i < border; i++) {
|
|
4451
|
+
input.types.unshift(Array.from({ length: newSize }, (_) => b));
|
|
4452
|
+
input.types.push(Array.from({ length: newSize }, (_) => b));
|
|
4453
|
+
}
|
|
4454
|
+
return input;
|
|
4455
|
+
}
|
|
4456
|
+
function getDataAt(data, x, y, defaults = false) {
|
|
4457
|
+
if (x < 0 || y < 0 || x >= data.length || y >= data.length)
|
|
4458
|
+
return defaults;
|
|
4459
|
+
return data[y][x];
|
|
4460
|
+
}
|
|
4461
|
+
function renderUnicodeCompact(data, options = {}) {
|
|
4462
|
+
const palette = {
|
|
4463
|
+
WHITE_ALL: "\u2588",
|
|
4464
|
+
WHITE_BLACK: "\u2580",
|
|
4465
|
+
BLACK_WHITE: "\u2584",
|
|
4466
|
+
BLACK_ALL: " "
|
|
4467
|
+
};
|
|
4468
|
+
const result = encode(data, options);
|
|
4469
|
+
const WHITE = false;
|
|
4470
|
+
const BLACK = true;
|
|
4471
|
+
const at = (x, y) => getDataAt(result.data, x, y, true);
|
|
4472
|
+
const lines = [];
|
|
4473
|
+
let line = "";
|
|
4474
|
+
for (let row = 0; row < result.size; row += 2) {
|
|
4475
|
+
for (let col = 0; col < result.size; col++) {
|
|
4476
|
+
if (at(col, row) === WHITE && at(col, row + 1) === WHITE)
|
|
4477
|
+
line += palette.WHITE_ALL;
|
|
4478
|
+
else if (at(col, row) === WHITE && at(col, row + 1) === BLACK)
|
|
4479
|
+
line += palette.WHITE_BLACK;
|
|
4480
|
+
else if (at(col, row) === BLACK && at(col, row + 1) === WHITE)
|
|
4481
|
+
line += palette.BLACK_WHITE;
|
|
4482
|
+
else
|
|
4483
|
+
line += palette.BLACK_ALL;
|
|
4484
|
+
}
|
|
4485
|
+
lines.push(line);
|
|
4486
|
+
line = "";
|
|
4487
|
+
}
|
|
4488
|
+
return lines.join("\n");
|
|
4489
|
+
}
|
|
4490
|
+
|
|
3800
4491
|
// ../tutticode-protocol/src/messages.ts
|
|
3801
4492
|
var BRIDGE_PROTOCOL = 1;
|
|
3802
4493
|
var BRIDGE_PATH = "/bridge";
|
|
@@ -3884,6 +4575,42 @@ function isFrameHeader(value) {
|
|
|
3884
4575
|
return typeof v.id === "string" && typeof v.part === "string" && Number.isInteger(v.seq) && v.seq >= 0 && typeof v.last === "boolean";
|
|
3885
4576
|
}
|
|
3886
4577
|
|
|
4578
|
+
// ../tutticode-protocol/src/auth.ts
|
|
4579
|
+
var TUTTICODE_SCOPE = "tutticode";
|
|
4580
|
+
var TUTTICODE_RESOURCE_PATH = "/api/tutticode";
|
|
4581
|
+
function tutticodeResourceUrl(origin) {
|
|
4582
|
+
return `${origin}${TUTTICODE_RESOURCE_PATH}`;
|
|
4583
|
+
}
|
|
4584
|
+
var TUTTICODE_CLIENT_ID = "tutticode-cli";
|
|
4585
|
+
var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
4586
|
+
var TUTTICODE_OAUTH_PATHS = {
|
|
4587
|
+
token: "/api/oauth/token",
|
|
4588
|
+
revoke: "/api/oauth/revoke",
|
|
4589
|
+
deviceAuthorization: "/api/oauth/device_authorization",
|
|
4590
|
+
/** Browser pages (lane B). */
|
|
4591
|
+
login: "/code/login",
|
|
4592
|
+
activate: "/code/activate",
|
|
4593
|
+
/** The terms of use page (the site has no `/terms`; it redirects here). */
|
|
4594
|
+
terms: "/page/terms-of-use"
|
|
4595
|
+
};
|
|
4596
|
+
function tutticodeTermsUrl(origin) {
|
|
4597
|
+
return `${origin}${TUTTICODE_OAUTH_PATHS.terms}`;
|
|
4598
|
+
}
|
|
4599
|
+
var TUTTICODE_LOGIN_HINT_COMMAND = "tutticode login --device --no-wait";
|
|
4600
|
+
var DEFAULT_TUTTICODE_LIMITS = {
|
|
4601
|
+
enforce: false,
|
|
4602
|
+
anonMaxSeconds: 60,
|
|
4603
|
+
anonWatermarkAfterSeconds: 30,
|
|
4604
|
+
watermarkGapSeconds: 3,
|
|
4605
|
+
signedInMaxSeconds: 300,
|
|
4606
|
+
signedInDailyRenders: 1e3,
|
|
4607
|
+
minCliVersion: "0.2.0"
|
|
4608
|
+
};
|
|
4609
|
+
var VERSION = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9A-Za-z.-]{1,32})?$/;
|
|
4610
|
+
function isCliVersion(v) {
|
|
4611
|
+
return typeof v === "string" && VERSION.test(v);
|
|
4612
|
+
}
|
|
4613
|
+
|
|
3887
4614
|
// ../tutticode-protocol/src/validate.ts
|
|
3888
4615
|
var BRIDGE_MAX_CODE_CHARS = 2e5;
|
|
3889
4616
|
var FORMATS = ["wav", "flac", "mp3"];
|
|
@@ -3932,11 +4659,28 @@ function validRender(m) {
|
|
|
3932
4659
|
return "lufs must be -40..-5";
|
|
3933
4660
|
if (!optional(m.truePeak, (v) => finite(v) && v >= -12 && v <= 0))
|
|
3934
4661
|
return "truePeak must be -12..0";
|
|
3935
|
-
for (const key of ["stems", "analyze"])
|
|
4662
|
+
for (const key of ["stems", "analyze", "limit", "brandTag"])
|
|
3936
4663
|
if (!optional(m[key], (v) => typeof v === "boolean"))
|
|
3937
4664
|
return `${key} must be a boolean`;
|
|
3938
4665
|
return void 0;
|
|
3939
4666
|
}
|
|
4667
|
+
var MAX_TOKEN_CHARS = 4096;
|
|
4668
|
+
var TOKEN = /^[A-Za-z0-9._~+/=-]+$/;
|
|
4669
|
+
var INSTALL_ID = /^[A-Za-z0-9_-]{16,64}$/;
|
|
4670
|
+
function validAuth(m) {
|
|
4671
|
+
if (!optional(
|
|
4672
|
+
m.accessToken,
|
|
4673
|
+
(v) => typeof v === "string" && v.length > 0 && v.length <= MAX_TOKEN_CHARS && TOKEN.test(v)
|
|
4674
|
+
))
|
|
4675
|
+
return "auth.accessToken must be a token string";
|
|
4676
|
+
if (typeof m.installId !== "string" || !INSTALL_ID.test(m.installId))
|
|
4677
|
+
return "auth.installId must be 16..64 characters of [A-Za-z0-9_-]";
|
|
4678
|
+
if (!isCliVersion(m.cliVersion))
|
|
4679
|
+
return "auth.cliVersion must be major.minor.patch";
|
|
4680
|
+
if (m.client !== "cli" && m.client !== "mcp")
|
|
4681
|
+
return "auth.client must be cli or mcp";
|
|
4682
|
+
return void 0;
|
|
4683
|
+
}
|
|
3940
4684
|
function validateCommand(raw) {
|
|
3941
4685
|
if (!isObject(raw))
|
|
3942
4686
|
return { ok: false, reason: "not an object" };
|
|
@@ -3955,6 +4699,19 @@ function validateCommand(raw) {
|
|
|
3955
4699
|
return validCode(raw.code) ? { ok: true, value: raw } : { ok: false, reason: "open needs code" };
|
|
3956
4700
|
case "cancel":
|
|
3957
4701
|
return isId(raw.id) ? { ok: true, value: raw } : { ok: false, reason: "cancel needs an id" };
|
|
4702
|
+
case "auth": {
|
|
4703
|
+
const reason = validAuth(raw);
|
|
4704
|
+
if (reason)
|
|
4705
|
+
return { ok: false, reason };
|
|
4706
|
+
const auth = {
|
|
4707
|
+
type: "auth",
|
|
4708
|
+
...raw.accessToken !== void 0 ? { accessToken: raw.accessToken } : {},
|
|
4709
|
+
installId: raw.installId,
|
|
4710
|
+
cliVersion: raw.cliVersion,
|
|
4711
|
+
client: raw.client
|
|
4712
|
+
};
|
|
4713
|
+
return { ok: true, value: auth };
|
|
4714
|
+
}
|
|
3958
4715
|
default:
|
|
3959
4716
|
return { ok: false, reason: `unknown command type ${String(raw.type)}` };
|
|
3960
4717
|
}
|
|
@@ -3971,6 +4728,8 @@ function validateMessage(raw) {
|
|
|
3971
4728
|
if (raw.type === "error") {
|
|
3972
4729
|
if (typeof raw.code !== "string" || typeof raw.message !== "string")
|
|
3973
4730
|
return { ok: false, reason: "error needs code and message" };
|
|
4731
|
+
if (!optional(raw.limit, validLimit))
|
|
4732
|
+
return { ok: false, reason: "error.limit must be {name, value, signedInValue}" };
|
|
3974
4733
|
return { ok: true, value: raw };
|
|
3975
4734
|
}
|
|
3976
4735
|
if (!isId(raw.id))
|
|
@@ -3983,11 +4742,16 @@ function validateMessage(raw) {
|
|
|
3983
4742
|
return { ok: false, reason: "rendered needs meta" };
|
|
3984
4743
|
return { ok: true, value: raw };
|
|
3985
4744
|
}
|
|
4745
|
+
function validLimit(v) {
|
|
4746
|
+
return isObject(v) && v.name === "length" && inRange(v.value, 0, MAX_RENDER_SECONDS) && inRange(v.signedInValue, 0, MAX_RENDER_SECONDS);
|
|
4747
|
+
}
|
|
3986
4748
|
function bridgePageUrl(origin, locale, port, token) {
|
|
3987
4749
|
return `${origin}/${locale}/code/bridge#p=${port}&t=${encodeURIComponent(token)}`;
|
|
3988
4750
|
}
|
|
3989
4751
|
var MAX_LABEL_CHARS = 64;
|
|
3990
4752
|
var MAX_VERSION_CHARS = 128;
|
|
4753
|
+
var MAX_WARNINGS = 32;
|
|
4754
|
+
var MAX_WARNING_CHARS = 1e3;
|
|
3991
4755
|
var MAX_EDIT_URL_CHARS = 1e6;
|
|
3992
4756
|
var MAX_GRID_ENTRIES = 2e4;
|
|
3993
4757
|
var MAX_RENDER_SECONDS = BRIDGE_MAX_SECONDS + 60;
|
|
@@ -4023,6 +4787,13 @@ function validateRenderMeta(raw, expect) {
|
|
|
4023
4787
|
return no("meta.lufs / meta.truePeak out of range");
|
|
4024
4788
|
if (!inRange(raw.gainDb, -100, 100))
|
|
4025
4789
|
return no("meta.gainDb out of range");
|
|
4790
|
+
if (!optional(raw.limitDb, (v) => inRange(v, 0, 100)))
|
|
4791
|
+
return no("meta.limitDb out of range");
|
|
4792
|
+
if (!optional(
|
|
4793
|
+
raw.warnings,
|
|
4794
|
+
(v) => Array.isArray(v) && v.length <= MAX_WARNINGS && v.every((w) => typeof w === "string" && w.length <= MAX_WARNING_CHARS)
|
|
4795
|
+
))
|
|
4796
|
+
return no("meta.warnings must be at most 32 short strings");
|
|
4026
4797
|
const barTimes = grid(raw.barTimes);
|
|
4027
4798
|
const beatTimes = grid(raw.beatTimes);
|
|
4028
4799
|
if (!barTimes || !beatTimes)
|
|
@@ -4046,7 +4817,14 @@ function validateRenderMeta(raw, expect) {
|
|
|
4046
4817
|
const voicePeak = level(v.truePeak, -200, 40);
|
|
4047
4818
|
if (voiceLufs === void 0 || voicePeak === void 0)
|
|
4048
4819
|
return no("meta.voices levels out of range");
|
|
4049
|
-
|
|
4820
|
+
if (!optional(v.gainDb, (g) => inRange(g, -100, 100)))
|
|
4821
|
+
return no("meta.voices gainDb out of range");
|
|
4822
|
+
voices.push({
|
|
4823
|
+
label: v.label,
|
|
4824
|
+
lufs: voiceLufs,
|
|
4825
|
+
truePeak: voicePeak,
|
|
4826
|
+
...v.gainDb !== void 0 ? { gainDb: v.gainDb } : {}
|
|
4827
|
+
});
|
|
4050
4828
|
}
|
|
4051
4829
|
if (!isObject(raw.check))
|
|
4052
4830
|
return no("meta.check must be a report");
|
|
@@ -4054,6 +4832,12 @@ function validateRenderMeta(raw, expect) {
|
|
|
4054
4832
|
return no("meta.analysis must be an object");
|
|
4055
4833
|
if (typeof raw.engineVersion !== "string" || raw.engineVersion.length > MAX_VERSION_CHARS)
|
|
4056
4834
|
return no("meta.engineVersion malformed");
|
|
4835
|
+
let watermark;
|
|
4836
|
+
if (raw.watermark !== void 0) {
|
|
4837
|
+
watermark = validWatermark(raw.watermark, raw.seconds);
|
|
4838
|
+
if (!watermark)
|
|
4839
|
+
return no("meta.watermark must be {reason, at: ascending seconds within the render, removeWith}");
|
|
4840
|
+
}
|
|
4057
4841
|
if (typeof raw.editUrl !== "string" || raw.editUrl.length > MAX_EDIT_URL_CHARS || !expect.origins.includes(urlOrigin(raw.editUrl) ?? ""))
|
|
4058
4842
|
return no("meta.editUrl must be a link on the TuttiDAW site");
|
|
4059
4843
|
return {
|
|
@@ -4067,6 +4851,8 @@ function validateRenderMeta(raw, expect) {
|
|
|
4067
4851
|
lufs,
|
|
4068
4852
|
truePeak,
|
|
4069
4853
|
gainDb: raw.gainDb,
|
|
4854
|
+
...raw.limitDb !== void 0 ? { limitDb: raw.limitDb } : {},
|
|
4855
|
+
...raw.warnings !== void 0 ? { warnings: [...raw.warnings] } : {},
|
|
4070
4856
|
barTimes,
|
|
4071
4857
|
beatTimes,
|
|
4072
4858
|
stems,
|
|
@@ -4074,10 +4860,28 @@ function validateRenderMeta(raw, expect) {
|
|
|
4074
4860
|
...raw.analysis !== void 0 ? { analysis: raw.analysis } : {},
|
|
4075
4861
|
check: raw.check,
|
|
4076
4862
|
editUrl: raw.editUrl,
|
|
4077
|
-
engineVersion: raw.engineVersion
|
|
4863
|
+
engineVersion: raw.engineVersion,
|
|
4864
|
+
...watermark ? { watermark } : {}
|
|
4078
4865
|
}
|
|
4079
4866
|
};
|
|
4080
4867
|
}
|
|
4868
|
+
var MAX_WATERMARKS = 200;
|
|
4869
|
+
function validWatermark(v, seconds) {
|
|
4870
|
+
if (!isObject(v) || v.reason !== "anonymous")
|
|
4871
|
+
return void 0;
|
|
4872
|
+
if (typeof v.removeWith !== "string" || !v.removeWith || v.removeWith.length > 64)
|
|
4873
|
+
return void 0;
|
|
4874
|
+
if (!Array.isArray(v.at) || v.at.length === 0 || v.at.length > MAX_WATERMARKS)
|
|
4875
|
+
return void 0;
|
|
4876
|
+
const at = v.at;
|
|
4877
|
+
for (let i = 0; i < at.length; i++) {
|
|
4878
|
+
if (!inRange(at[i], 0, seconds))
|
|
4879
|
+
return void 0;
|
|
4880
|
+
if (i > 0 && at[i] <= at[i - 1])
|
|
4881
|
+
return void 0;
|
|
4882
|
+
}
|
|
4883
|
+
return { reason: "anonymous", at: [...at], removeWith: v.removeWith };
|
|
4884
|
+
}
|
|
4081
4885
|
function urlOrigin(url) {
|
|
4082
4886
|
try {
|
|
4083
4887
|
const parsed = new URL(url);
|
|
@@ -4087,22 +4891,677 @@ function urlOrigin(url) {
|
|
|
4087
4891
|
}
|
|
4088
4892
|
}
|
|
4089
4893
|
|
|
4894
|
+
// src/credentials.ts
|
|
4895
|
+
var import_node_child_process = require("node:child_process");
|
|
4896
|
+
var import_node_crypto = require("node:crypto");
|
|
4897
|
+
var import_node_fs2 = require("node:fs");
|
|
4898
|
+
var import_node_os2 = require("node:os");
|
|
4899
|
+
var import_node_path2 = require("node:path");
|
|
4900
|
+
|
|
4901
|
+
// src/env.ts
|
|
4902
|
+
var import_node_fs = require("node:fs");
|
|
4903
|
+
var import_node_os = require("node:os");
|
|
4904
|
+
var import_node_path = require("node:path");
|
|
4905
|
+
var VERSION2 = true ? "0.2.0" : readPackageVersion();
|
|
4906
|
+
function envBrowserArgs() {
|
|
4907
|
+
const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
|
|
4908
|
+
return args?.length ? args : void 0;
|
|
4909
|
+
}
|
|
4910
|
+
function stateDir() {
|
|
4911
|
+
const dir = process.env.TUTTICODE_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".tutticode");
|
|
4912
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
4913
|
+
return dir;
|
|
4914
|
+
}
|
|
4915
|
+
var assetsDir;
|
|
4916
|
+
function skillsDir() {
|
|
4917
|
+
return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
|
|
4918
|
+
(0, import_node_path.join)(__dirname, "..", "..", "..", "public", "skills");
|
|
4919
|
+
}
|
|
4920
|
+
function setSkillsDir(dir) {
|
|
4921
|
+
assetsDir = dir;
|
|
4922
|
+
}
|
|
4923
|
+
var LOG_ROLL_BYTES = 2 * 1024 * 1024;
|
|
4924
|
+
function createLogger(name, verbose) {
|
|
4925
|
+
let file2;
|
|
4926
|
+
try {
|
|
4927
|
+
const dir = (0, import_node_path.join)(stateDir(), "logs");
|
|
4928
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
4929
|
+
file2 = (0, import_node_path.join)(dir, `${name}.log`);
|
|
4930
|
+
if (((0, import_node_fs.statSync)(file2, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
|
|
4931
|
+
(0, import_node_fs.renameSync)(file2, `${file2}.1`);
|
|
4932
|
+
} catch {
|
|
4933
|
+
file2 = void 0;
|
|
4934
|
+
}
|
|
4935
|
+
return (level2, message) => {
|
|
4936
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
|
|
4937
|
+
if (verbose || level2 === "error")
|
|
4938
|
+
process.stderr.write(`tutticode: ${message}
|
|
4939
|
+
`);
|
|
4940
|
+
if (!file2)
|
|
4941
|
+
return;
|
|
4942
|
+
try {
|
|
4943
|
+
(0, import_node_fs.appendFileSync)(file2, `${line}
|
|
4944
|
+
`);
|
|
4945
|
+
} catch {
|
|
4946
|
+
}
|
|
4947
|
+
};
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
// src/credentials.ts
|
|
4951
|
+
var CREDENTIALS_FILE = "credentials";
|
|
4952
|
+
function credentialsPath() {
|
|
4953
|
+
return (0, import_node_path2.join)(stateDir(), CREDENTIALS_FILE);
|
|
4954
|
+
}
|
|
4955
|
+
var EMPTY = { version: 1, accounts: {} };
|
|
4956
|
+
function readCredentials(path = credentialsPath()) {
|
|
4957
|
+
let raw;
|
|
4958
|
+
try {
|
|
4959
|
+
if ((0, import_node_fs2.lstatSync)(path, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
4960
|
+
return EMPTY;
|
|
4961
|
+
raw = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
4962
|
+
} catch {
|
|
4963
|
+
return EMPTY;
|
|
4964
|
+
}
|
|
4965
|
+
if (!isObject2(raw) || raw.version !== 1 || !isObject2(raw.accounts))
|
|
4966
|
+
return EMPTY;
|
|
4967
|
+
const accounts = {};
|
|
4968
|
+
for (const [origin, entry] of Object.entries(raw.accounts))
|
|
4969
|
+
if (validAccount(entry))
|
|
4970
|
+
accounts[origin] = entry;
|
|
4971
|
+
return { version: 1, accounts };
|
|
4972
|
+
}
|
|
4973
|
+
function readAccount(origin) {
|
|
4974
|
+
return readCredentials().accounts[origin];
|
|
4975
|
+
}
|
|
4976
|
+
function validAccount(v) {
|
|
4977
|
+
return isObject2(v) && v.kind === "login" && typeof v.accessToken === "string" && typeof v.refreshToken === "string" && typeof v.accessTokenExpiresAt === "string" && typeof v.clientId === "string" && typeof v.scope === "string";
|
|
4978
|
+
}
|
|
4979
|
+
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4980
|
+
function writeCredentials(file2) {
|
|
4981
|
+
const path = credentialsPath();
|
|
4982
|
+
if (!Object.keys(file2.accounts).length) {
|
|
4983
|
+
(0, import_node_fs2.rmSync)(path, { force: true });
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
writePrivateFile(path, `${JSON.stringify(file2, null, 2)}
|
|
4987
|
+
`);
|
|
4988
|
+
}
|
|
4989
|
+
function saveAccount(origin, account) {
|
|
4990
|
+
return withCredentialsLock(() => {
|
|
4991
|
+
const accounts = { ...readCredentials().accounts };
|
|
4992
|
+
if (account)
|
|
4993
|
+
accounts[origin] = account;
|
|
4994
|
+
else
|
|
4995
|
+
delete accounts[origin];
|
|
4996
|
+
writeCredentials({ version: 1, accounts });
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
var PrivateDirError = class extends Error {
|
|
5000
|
+
};
|
|
5001
|
+
var privateDirs = /* @__PURE__ */ new Map();
|
|
5002
|
+
var fileId = (st) => `${st.dev}:${st.ino}`;
|
|
5003
|
+
function refusal(dir, why) {
|
|
5004
|
+
return new PrivateDirError(
|
|
5005
|
+
`${dir} ${why}, so tutticode will not keep a sign-in there. Point TUTTICODE_HOME at a folder you own that only you can use (or unset it for ~/.tutticode).`
|
|
5006
|
+
);
|
|
5007
|
+
}
|
|
5008
|
+
function ensurePrivateDir(dir = stateDir()) {
|
|
5009
|
+
const known = privateDirs.get(dir);
|
|
5010
|
+
const now = (0, import_node_fs2.lstatSync)(dir, { bigint: true, throwIfNoEntry: false });
|
|
5011
|
+
if (known && now && fileId(now) === known)
|
|
5012
|
+
return dir;
|
|
5013
|
+
privateDirs.set(dir, checkPrivateDir(dir, true));
|
|
5014
|
+
return dir;
|
|
5015
|
+
}
|
|
5016
|
+
function checkPrivateDir(dir, fix) {
|
|
5017
|
+
const st = (0, import_node_fs2.lstatSync)(dir, { bigint: true, throwIfNoEntry: false });
|
|
5018
|
+
if (!st)
|
|
5019
|
+
throw refusal(dir, "does not exist");
|
|
5020
|
+
if (st.isSymbolicLink())
|
|
5021
|
+
throw refusal(dir, "is a symbolic link or a junction");
|
|
5022
|
+
if (!st.isDirectory())
|
|
5023
|
+
throw refusal(dir, "is not a folder");
|
|
5024
|
+
const id = process.platform === "win32" ? checkWindowsDir(dir, fix) : checkPosixDir(dir, fix);
|
|
5025
|
+
if (id !== fileId(st))
|
|
5026
|
+
throw refusal(dir, "was replaced while it was checked");
|
|
5027
|
+
return id;
|
|
5028
|
+
}
|
|
5029
|
+
function checkPosixDir(dir, fix) {
|
|
5030
|
+
let fd;
|
|
5031
|
+
try {
|
|
5032
|
+
fd = (0, import_node_fs2.openSync)(
|
|
5033
|
+
dir,
|
|
5034
|
+
import_node_fs2.constants.O_RDONLY | import_node_fs2.constants.O_DIRECTORY | import_node_fs2.constants.O_NOFOLLOW
|
|
5035
|
+
);
|
|
5036
|
+
} catch {
|
|
5037
|
+
throw refusal(dir, "was replaced while it was checked");
|
|
5038
|
+
}
|
|
5039
|
+
try {
|
|
5040
|
+
let st = (0, import_node_fs2.fstatSync)(fd, { bigint: true });
|
|
5041
|
+
if (process.getuid && st.uid !== BigInt(process.getuid()))
|
|
5042
|
+
throw refusal(dir, "belongs to another user");
|
|
5043
|
+
if (fix && st.mode & BigInt(63)) {
|
|
5044
|
+
try {
|
|
5045
|
+
(0, import_node_fs2.fchmodSync)(fd, 448);
|
|
5046
|
+
} catch {
|
|
5047
|
+
throw refusal(dir, "cannot be made private (chmod 0700 failed)");
|
|
5048
|
+
}
|
|
5049
|
+
st = (0, import_node_fs2.fstatSync)(fd, { bigint: true });
|
|
5050
|
+
}
|
|
5051
|
+
if (st.mode & BigInt(63))
|
|
5052
|
+
throw refusal(dir, "is open to other users");
|
|
5053
|
+
return fileId(st);
|
|
5054
|
+
} finally {
|
|
5055
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
var WINDOWS_OWNERS = /* @__PURE__ */ new Set(["SY", "BA", "S-1-5-18", "S-1-5-32-544"]);
|
|
5059
|
+
function checkWindowsDir(dir, fix) {
|
|
5060
|
+
const sid = currentUserSid();
|
|
5061
|
+
const inspect = () => {
|
|
5062
|
+
const sec = windowsSecurity(dir);
|
|
5063
|
+
if (!sid || !sec)
|
|
5064
|
+
throw refusal(dir, "has an owner or access list tutticode cannot check");
|
|
5065
|
+
if (sec.reparse)
|
|
5066
|
+
throw refusal(dir, "is a symbolic link or a junction");
|
|
5067
|
+
if (sec.owner !== sid && !WINDOWS_OWNERS.has(sec.owner))
|
|
5068
|
+
throw refusal(dir, `belongs to another user (${sec.owner})`);
|
|
5069
|
+
return { access: daclAccess(sec.sddl, sid), protected: sec.protected };
|
|
5070
|
+
};
|
|
5071
|
+
let state = inspect();
|
|
5072
|
+
if (fix && (!state.access.ok || !state.protected)) {
|
|
5073
|
+
if (!restrictDirAcl(dir))
|
|
5074
|
+
throw refusal(dir, "could not be limited to your user (icacls failed)");
|
|
5075
|
+
state = inspect();
|
|
5076
|
+
}
|
|
5077
|
+
if (!state.access.ok)
|
|
5078
|
+
throw refusal(dir, `is ${state.access.detail}`);
|
|
5079
|
+
return fileId((0, import_node_fs2.lstatSync)(dir, { bigint: true }));
|
|
5080
|
+
}
|
|
5081
|
+
function writePrivateFile(path, text) {
|
|
5082
|
+
const dir = (0, import_node_path2.dirname)(path);
|
|
5083
|
+
ensurePrivateDir(dir);
|
|
5084
|
+
const checked = privateDirs.get(dir);
|
|
5085
|
+
const target = (0, import_node_fs2.lstatSync)(path, { throwIfNoEntry: false });
|
|
5086
|
+
if (target && !target.isFile())
|
|
5087
|
+
throw new PrivateDirError(
|
|
5088
|
+
`${path} is not a plain file (a link, a junction or a folder), so tutticode will not write the sign-in through it. Remove it and try again.`
|
|
5089
|
+
);
|
|
5090
|
+
const changed = () => new PrivateDirError(
|
|
5091
|
+
`${dir} changed while the sign-in was being written (replaced, or its owner or access list changed); nothing was written.`
|
|
5092
|
+
);
|
|
5093
|
+
const tmp = `${path}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
|
|
5094
|
+
const tmpId = () => {
|
|
5095
|
+
const st = (0, import_node_fs2.lstatSync)(tmp, { bigint: true, throwIfNoEntry: false });
|
|
5096
|
+
return st && fileId(st);
|
|
5097
|
+
};
|
|
5098
|
+
const fd = (0, import_node_fs2.openSync)(
|
|
5099
|
+
tmp,
|
|
5100
|
+
import_node_fs2.constants.O_WRONLY | import_node_fs2.constants.O_CREAT | import_node_fs2.constants.O_EXCL | (import_node_fs2.constants.O_NOFOLLOW ?? 0),
|
|
5101
|
+
384
|
|
5102
|
+
);
|
|
5103
|
+
let open2 = true;
|
|
5104
|
+
try {
|
|
5105
|
+
const written = fileId((0, import_node_fs2.fstatSync)(fd, { bigint: true }));
|
|
5106
|
+
if (tmpId() !== written)
|
|
5107
|
+
throw changed();
|
|
5108
|
+
(0, import_node_fs2.writeFileSync)(fd, text);
|
|
5109
|
+
(0, import_node_fs2.fsyncSync)(fd);
|
|
5110
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5111
|
+
open2 = false;
|
|
5112
|
+
if (checkPrivateDir(dir, false) !== checked || tmpId() !== written)
|
|
5113
|
+
throw changed();
|
|
5114
|
+
if (process.platform === "win32" && !checkFileAccess(tmp)?.ok)
|
|
5115
|
+
throw new PrivateDirError(
|
|
5116
|
+
`Could not confirm that ${tmp} is private to your user; nothing was written.`
|
|
5117
|
+
);
|
|
5118
|
+
renameWithRetry(tmp, path);
|
|
5119
|
+
} catch (e) {
|
|
5120
|
+
if (open2)
|
|
5121
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5122
|
+
(0, import_node_fs2.rmSync)(tmp, { force: true });
|
|
5123
|
+
throw e;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
function renameWithRetry(from, to) {
|
|
5127
|
+
for (let attempt = 0; ; attempt++) {
|
|
5128
|
+
try {
|
|
5129
|
+
(0, import_node_fs2.renameSync)(from, to);
|
|
5130
|
+
return;
|
|
5131
|
+
} catch (e) {
|
|
5132
|
+
const code = e.code;
|
|
5133
|
+
if (attempt >= 20 || code !== "EPERM" && code !== "EBUSY")
|
|
5134
|
+
throw e;
|
|
5135
|
+
sleepSync(25);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
function sleepSync(ms) {
|
|
5140
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5141
|
+
}
|
|
5142
|
+
var system32 = (exe) => (0, import_node_path2.join)(process.env.SystemRoot ?? "C:\\Windows", "System32", exe);
|
|
5143
|
+
var userSid;
|
|
5144
|
+
function currentUserSid() {
|
|
5145
|
+
if (userSid !== void 0)
|
|
5146
|
+
return userSid;
|
|
5147
|
+
const out = (0, import_node_child_process.spawnSync)(
|
|
5148
|
+
system32("whoami.exe"),
|
|
5149
|
+
["/user", "/fo", "csv", "/nh"],
|
|
5150
|
+
{
|
|
5151
|
+
encoding: "utf8",
|
|
5152
|
+
windowsHide: true
|
|
5153
|
+
}
|
|
5154
|
+
).stdout;
|
|
5155
|
+
userSid = /"(S-1-[0-9-]+)"/.exec(out ?? "")?.[1] ?? null;
|
|
5156
|
+
return userSid;
|
|
5157
|
+
}
|
|
5158
|
+
function icaclsUser() {
|
|
5159
|
+
const sid = currentUserSid();
|
|
5160
|
+
return sid ? `*${sid}` : process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${process.env.USERNAME}` : process.env.USERNAME ?? "";
|
|
5161
|
+
}
|
|
5162
|
+
var icacls = (args) => (0, import_node_child_process.spawnSync)(system32("icacls.exe"), args, {
|
|
5163
|
+
encoding: "utf8",
|
|
5164
|
+
windowsHide: true
|
|
5165
|
+
}).status === 0;
|
|
5166
|
+
function restrictDirAcl(dir) {
|
|
5167
|
+
const user = icaclsUser();
|
|
5168
|
+
return !!user && icacls([
|
|
5169
|
+
dir,
|
|
5170
|
+
"/inheritance:r",
|
|
5171
|
+
"/grant:r",
|
|
5172
|
+
`${user}:(OI)(CI)F`,
|
|
5173
|
+
"*S-1-5-18:(OI)(CI)F",
|
|
5174
|
+
"*S-1-5-32-544:(OI)(CI)F"
|
|
5175
|
+
]);
|
|
5176
|
+
}
|
|
5177
|
+
var PS_SECURITY = "$p = $env:TUTTICODE_ACL_PATH; $a = [int][IO.File]::GetAttributes($p); $s = (New-Object System.Security.AccessControl.DirectorySecurity($p, 'Owner,Access')).GetSecurityDescriptorSddlForm('Owner,Access'); [Console]::Out.Write([string]$a + [char]10 + $s)";
|
|
5178
|
+
function windowsSecurity(path) {
|
|
5179
|
+
const out = (0, import_node_child_process.spawnSync)(
|
|
5180
|
+
system32("WindowsPowerShell\\v1.0\\powershell.exe"),
|
|
5181
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", PS_SECURITY],
|
|
5182
|
+
{
|
|
5183
|
+
encoding: "utf8",
|
|
5184
|
+
windowsHide: true,
|
|
5185
|
+
env: { ...process.env, TUTTICODE_ACL_PATH: path }
|
|
5186
|
+
}
|
|
5187
|
+
);
|
|
5188
|
+
if (out.status !== 0)
|
|
5189
|
+
return void 0;
|
|
5190
|
+
const m = /^(\d+)\r?\n(O:(S-1-[0-9-]+|[A-Z]{2})D:[^\r\n]*)/.exec(
|
|
5191
|
+
out.stdout ?? ""
|
|
5192
|
+
);
|
|
5193
|
+
if (!m)
|
|
5194
|
+
return void 0;
|
|
5195
|
+
return {
|
|
5196
|
+
owner: m[3],
|
|
5197
|
+
sddl: m[2],
|
|
5198
|
+
protected: /D:[A-Z]*P/.test(m[2]),
|
|
5199
|
+
// FILE_ATTRIBUTE_REPARSE_POINT
|
|
5200
|
+
reparse: (Number(m[1]) & 1024) !== 0
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
var ALLOWED_SIDS = /* @__PURE__ */ new Set(["SY", "BA", "OW", "S-1-5-18", "S-1-5-32-544"]);
|
|
5204
|
+
function sddlAllowTrustees(sddl) {
|
|
5205
|
+
const trustees = [];
|
|
5206
|
+
for (const ace of sddl.matchAll(/\(([^)]*)\)/g)) {
|
|
5207
|
+
const fields = ace[1].split(";");
|
|
5208
|
+
if (fields[0] === "A" && fields[5])
|
|
5209
|
+
trustees.push(fields[5]);
|
|
5210
|
+
}
|
|
5211
|
+
return trustees;
|
|
5212
|
+
}
|
|
5213
|
+
function checkFileAccess(path) {
|
|
5214
|
+
const st = (0, import_node_fs2.lstatSync)(path);
|
|
5215
|
+
if (st.isSymbolicLink())
|
|
5216
|
+
return { ok: false, detail: "a symbolic link or a junction" };
|
|
5217
|
+
if (process.platform !== "win32") {
|
|
5218
|
+
const mode = st.mode & 511;
|
|
5219
|
+
return mode & 63 ? { ok: false, detail: `mode ${mode.toString(8).padStart(4, "0")}` } : { ok: true, detail: "mode 0600" };
|
|
5220
|
+
}
|
|
5221
|
+
const sid = currentUserSid();
|
|
5222
|
+
if (!sid)
|
|
5223
|
+
return void 0;
|
|
5224
|
+
const dump = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `tutticode-acl-${process.pid}.tmp`);
|
|
5225
|
+
try {
|
|
5226
|
+
const saved = (0, import_node_child_process.spawnSync)(system32("icacls.exe"), [path, "/save", dump], {
|
|
5227
|
+
windowsHide: true
|
|
5228
|
+
});
|
|
5229
|
+
if (saved.status !== 0)
|
|
5230
|
+
return void 0;
|
|
5231
|
+
return daclAccess((0, import_node_fs2.readFileSync)(dump).toString("utf16le"), sid);
|
|
5232
|
+
} finally {
|
|
5233
|
+
(0, import_node_fs2.rmSync)(dump, { force: true });
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
function daclAccess(sddl, sid) {
|
|
5237
|
+
const others = sddlAllowTrustees(sddl).filter(
|
|
5238
|
+
(t) => t !== sid && !ALLOWED_SIDS.has(t)
|
|
5239
|
+
);
|
|
5240
|
+
return others.length ? { ok: false, detail: `also readable by ${others.join(", ")}` } : { ok: true, detail: "only you can read it" };
|
|
5241
|
+
}
|
|
5242
|
+
function fixFileAccess(path) {
|
|
5243
|
+
if (process.platform !== "win32") {
|
|
5244
|
+
(0, import_node_fs2.chmodSync)(path, 384);
|
|
5245
|
+
return true;
|
|
5246
|
+
}
|
|
5247
|
+
writePrivateFile(path, (0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
5248
|
+
return checkFileAccess(path)?.ok ?? false;
|
|
5249
|
+
}
|
|
5250
|
+
var LOCK_STALE_MS = 3e4;
|
|
5251
|
+
async function withCredentialsLock(fn, options = {}) {
|
|
5252
|
+
const lock = (0, import_node_path2.join)(stateDir(), `${CREDENTIALS_FILE}.lock`);
|
|
5253
|
+
const staleMs = options.staleMs ?? LOCK_STALE_MS;
|
|
5254
|
+
for (; ; ) {
|
|
5255
|
+
if (takeLock(lock, staleMs))
|
|
5256
|
+
break;
|
|
5257
|
+
await new Promise((r2) => setTimeout(r2, options.pollMs ?? 100));
|
|
5258
|
+
}
|
|
5259
|
+
try {
|
|
5260
|
+
return await fn();
|
|
5261
|
+
} finally {
|
|
5262
|
+
(0, import_node_fs2.rmSync)(lock, { force: true });
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
function takeLock(lock, staleMs) {
|
|
5266
|
+
const mtime = (0, import_node_fs2.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs;
|
|
5267
|
+
if (mtime !== void 0 && Date.now() - mtime > staleMs)
|
|
5268
|
+
(0, import_node_fs2.rmSync)(lock, { force: true });
|
|
5269
|
+
try {
|
|
5270
|
+
const fd = (0, import_node_fs2.openSync)(lock, "wx", 384);
|
|
5271
|
+
(0, import_node_fs2.writeSync)(fd, String(process.pid));
|
|
5272
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
5273
|
+
return true;
|
|
5274
|
+
} catch {
|
|
5275
|
+
return false;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
var INSTALL_ID2 = /^[A-Za-z0-9_-]{16,64}$/;
|
|
5279
|
+
function installId() {
|
|
5280
|
+
const path = (0, import_node_path2.join)(stateDir(), "install-id");
|
|
5281
|
+
try {
|
|
5282
|
+
const id2 = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
|
|
5283
|
+
if (INSTALL_ID2.test(id2))
|
|
5284
|
+
return id2;
|
|
5285
|
+
} catch {
|
|
5286
|
+
}
|
|
5287
|
+
const id = (0, import_node_crypto.randomBytes)(16).toString("base64url");
|
|
5288
|
+
try {
|
|
5289
|
+
(0, import_node_fs2.writeFileSync)(path, `${id}
|
|
5290
|
+
`, { mode: 384, flag: "wx" });
|
|
5291
|
+
} catch (e) {
|
|
5292
|
+
if (e.code === "EEXIST")
|
|
5293
|
+
try {
|
|
5294
|
+
const theirs = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
|
|
5295
|
+
if (INSTALL_ID2.test(theirs))
|
|
5296
|
+
return theirs;
|
|
5297
|
+
(0, import_node_fs2.writeFileSync)(path, `${id}
|
|
5298
|
+
`, { mode: 384 });
|
|
5299
|
+
} catch {
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
return id;
|
|
5303
|
+
}
|
|
5304
|
+
function deviceName() {
|
|
5305
|
+
const os = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : process.platform === "linux" ? "Linux" : process.platform;
|
|
5306
|
+
return `${(0, import_node_os2.hostname)()} (${os})`.slice(0, 100);
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
// src/account.ts
|
|
5310
|
+
var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
5311
|
+
var ACCOUNT_PATH = "/studio/account";
|
|
5312
|
+
var SIGN_IN_EXPIRED = "Your TuttiCode sign-in expired after a year: run `tutticode login`.";
|
|
5313
|
+
var OAuthError = class extends Error {
|
|
5314
|
+
constructor(error, message, status, body = {}) {
|
|
5315
|
+
super(message);
|
|
5316
|
+
this.error = error;
|
|
5317
|
+
this.status = status;
|
|
5318
|
+
this.body = body;
|
|
5319
|
+
}
|
|
5320
|
+
/** `TUTTICODE_TOKEN_EXPIRED` etc. when the server sent one. */
|
|
5321
|
+
get code() {
|
|
5322
|
+
return typeof this.body.code === "string" ? this.body.code : void 0;
|
|
5323
|
+
}
|
|
5324
|
+
};
|
|
5325
|
+
var ApiUnreachable = class extends Error {
|
|
5326
|
+
};
|
|
5327
|
+
async function request(origin, path, init, deps) {
|
|
5328
|
+
try {
|
|
5329
|
+
return await (deps.fetch ?? fetch)(`${origin}${path}`, {
|
|
5330
|
+
...init,
|
|
5331
|
+
redirect: "error",
|
|
5332
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? 15e3)
|
|
5333
|
+
});
|
|
5334
|
+
} catch (e) {
|
|
5335
|
+
throw new ApiUnreachable(
|
|
5336
|
+
`${origin} did not answer (${e.message}).`
|
|
5337
|
+
);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
async function jsonBody(response) {
|
|
5341
|
+
try {
|
|
5342
|
+
const body = await response.json();
|
|
5343
|
+
return typeof body === "object" && body !== null ? body : {};
|
|
5344
|
+
} catch {
|
|
5345
|
+
return {};
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
async function postOAuth(origin, path, params, deps = {}) {
|
|
5349
|
+
const response = await request(
|
|
5350
|
+
origin,
|
|
5351
|
+
path,
|
|
5352
|
+
{
|
|
5353
|
+
method: "POST",
|
|
5354
|
+
headers: {
|
|
5355
|
+
"Content-Type": "application/json",
|
|
5356
|
+
Accept: "application/json"
|
|
5357
|
+
},
|
|
5358
|
+
body: JSON.stringify({ client_id: TUTTICODE_CLIENT_ID, ...params })
|
|
5359
|
+
},
|
|
5360
|
+
deps
|
|
5361
|
+
);
|
|
5362
|
+
const body = await jsonBody(response);
|
|
5363
|
+
if (response.ok)
|
|
5364
|
+
return body;
|
|
5365
|
+
if (response.status >= 500)
|
|
5366
|
+
throw new ApiUnreachable(`${origin} answered ${response.status}.`);
|
|
5367
|
+
const error = typeof body.error === "string" ? body.error : "invalid_request";
|
|
5368
|
+
if (response.status === 429 && error !== "slow_down")
|
|
5369
|
+
throw new ApiUnreachable(`${origin} is rate limiting (429).`);
|
|
5370
|
+
throw new OAuthError(
|
|
5371
|
+
error,
|
|
5372
|
+
typeof body.error_description === "string" ? body.error_description : error,
|
|
5373
|
+
response.status,
|
|
5374
|
+
body
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
function tokenResponse(body) {
|
|
5378
|
+
if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string" || typeof body.expires_in !== "number")
|
|
5379
|
+
throw new ApiUnreachable("The token endpoint sent no tokens.");
|
|
5380
|
+
return body;
|
|
5381
|
+
}
|
|
5382
|
+
function storedAccount(tokens, now, base = {}) {
|
|
5383
|
+
return {
|
|
5384
|
+
kind: "login",
|
|
5385
|
+
clientId: TUTTICODE_CLIENT_ID,
|
|
5386
|
+
...base,
|
|
5387
|
+
accessToken: tokens.access_token,
|
|
5388
|
+
accessTokenExpiresAt: new Date(
|
|
5389
|
+
now + tokens.expires_in * 1e3
|
|
5390
|
+
).toISOString(),
|
|
5391
|
+
refreshToken: tokens.refresh_token,
|
|
5392
|
+
// A refresh never extends the year; keep the first end we learnt.
|
|
5393
|
+
grantExpiresAt: base.grantExpiresAt ?? (tokens.refresh_token_expires_in ? new Date(now + tokens.refresh_token_expires_in * 1e3).toISOString() : void 0),
|
|
5394
|
+
scope: tokens.scope || TUTTICODE_SCOPE,
|
|
5395
|
+
createdAt: base.createdAt ?? new Date(now).toISOString()
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
var TOKEN2 = /^[A-Za-z0-9._~+/=-]{1,4096}$/;
|
|
5399
|
+
function envToken() {
|
|
5400
|
+
const token = process.env.TUTTICODE_TOKEN?.trim();
|
|
5401
|
+
return token && TOKEN2.test(token) ? token : void 0;
|
|
5402
|
+
}
|
|
5403
|
+
var expiresWithin = (iso, ms, now) => !iso || !(Date.parse(iso) - now > ms);
|
|
5404
|
+
async function resolveToken(origin, deps = {}, options = {}) {
|
|
5405
|
+
const pat = options.envToken === false ? void 0 : envToken();
|
|
5406
|
+
if (pat)
|
|
5407
|
+
return { kind: "pat", accessToken: pat };
|
|
5408
|
+
const now = deps.now ?? Date.now;
|
|
5409
|
+
const stored = readAccount(origin);
|
|
5410
|
+
if (!stored)
|
|
5411
|
+
return { kind: "anonymous" };
|
|
5412
|
+
if (stored.grantExpiresAt && Date.parse(stored.grantExpiresAt) <= now())
|
|
5413
|
+
return { kind: "anonymous", ended: SIGN_IN_EXPIRED };
|
|
5414
|
+
if (!expiresWithin(stored.accessTokenExpiresAt, REFRESH_MARGIN_MS, now()))
|
|
5415
|
+
return { kind: "login", accessToken: stored.accessToken };
|
|
5416
|
+
return withCredentialsLock(async () => {
|
|
5417
|
+
const current = readAccount(origin);
|
|
5418
|
+
if (!current)
|
|
5419
|
+
return { kind: "anonymous" };
|
|
5420
|
+
if (!expiresWithin(current.accessTokenExpiresAt, REFRESH_MARGIN_MS, now()))
|
|
5421
|
+
return { kind: "login", accessToken: current.accessToken };
|
|
5422
|
+
try {
|
|
5423
|
+
const tokens = tokenResponse(
|
|
5424
|
+
await postOAuth(
|
|
5425
|
+
origin,
|
|
5426
|
+
TUTTICODE_OAUTH_PATHS.token,
|
|
5427
|
+
{ grant_type: "refresh_token", refresh_token: current.refreshToken },
|
|
5428
|
+
deps
|
|
5429
|
+
)
|
|
5430
|
+
);
|
|
5431
|
+
writeCredentials({
|
|
5432
|
+
version: 1,
|
|
5433
|
+
accounts: {
|
|
5434
|
+
...readCredentials().accounts,
|
|
5435
|
+
[origin]: storedAccount(tokens, now(), current)
|
|
5436
|
+
}
|
|
5437
|
+
});
|
|
5438
|
+
return { kind: "login", accessToken: tokens.access_token };
|
|
5439
|
+
} catch (e) {
|
|
5440
|
+
if (e instanceof OAuthError)
|
|
5441
|
+
return {
|
|
5442
|
+
kind: "anonymous",
|
|
5443
|
+
ended: e.code === "TUTTICODE_TOKEN_EXPIRED" ? SIGN_IN_EXPIRED : "Your TuttiCode sign-in is no longer valid (signed out on the account page, or replaced): run `tutticode login`."
|
|
5444
|
+
};
|
|
5445
|
+
return { kind: "login", accessToken: current.accessToken };
|
|
5446
|
+
}
|
|
5447
|
+
});
|
|
5448
|
+
}
|
|
5449
|
+
function bridgeAuth(token, client) {
|
|
5450
|
+
return {
|
|
5451
|
+
...token.accessToken ? { accessToken: token.accessToken } : {},
|
|
5452
|
+
installId: installId(),
|
|
5453
|
+
cliVersion: VERSION2,
|
|
5454
|
+
client
|
|
5455
|
+
};
|
|
5456
|
+
}
|
|
5457
|
+
async function accountState(origin, deps = {}, options = {}) {
|
|
5458
|
+
const token = await resolveToken(origin, deps, options);
|
|
5459
|
+
if (!token.accessToken || token.kind === "anonymous")
|
|
5460
|
+
return token.ended ? {
|
|
5461
|
+
state: "rejected",
|
|
5462
|
+
tokenKind: "login",
|
|
5463
|
+
code: "TUTTICODE_TOKEN_EXPIRED",
|
|
5464
|
+
message: token.ended
|
|
5465
|
+
} : { state: "anonymous" };
|
|
5466
|
+
const tokenKind = token.kind;
|
|
5467
|
+
let response;
|
|
5468
|
+
try {
|
|
5469
|
+
response = await request(
|
|
5470
|
+
origin,
|
|
5471
|
+
"/api/tutticode/me",
|
|
5472
|
+
{
|
|
5473
|
+
headers: {
|
|
5474
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
5475
|
+
Accept: "application/json"
|
|
5476
|
+
}
|
|
5477
|
+
},
|
|
5478
|
+
deps
|
|
5479
|
+
);
|
|
5480
|
+
} catch (e) {
|
|
5481
|
+
return { state: "unreachable", tokenKind, message: e.message };
|
|
5482
|
+
}
|
|
5483
|
+
const body = await jsonBody(response);
|
|
5484
|
+
if (response.ok && isMe(body))
|
|
5485
|
+
return { state: "signed-in", tokenKind, me: body };
|
|
5486
|
+
if (response.status === 401 || response.status === 403)
|
|
5487
|
+
return {
|
|
5488
|
+
state: "rejected",
|
|
5489
|
+
tokenKind,
|
|
5490
|
+
...typeof body.code === "string" ? { code: body.code } : {},
|
|
5491
|
+
message: typeof body.message === "string" ? body.message : `The server refused the ${tokenKind === "pat" ? "TUTTICODE_TOKEN" : "sign-in"} (${response.status}).`
|
|
5492
|
+
};
|
|
5493
|
+
return {
|
|
5494
|
+
state: "unreachable",
|
|
5495
|
+
tokenKind,
|
|
5496
|
+
message: `${origin}/api/tutticode/me answered ${response.status}.`
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
function isMe(v) {
|
|
5500
|
+
const user = v.user;
|
|
5501
|
+
return typeof user === "object" && user !== null && typeof user.email === "string" && typeof v.plan === "string" && typeof v.limits === "object" && v.limits !== null;
|
|
5502
|
+
}
|
|
5503
|
+
function accountLimits(state) {
|
|
5504
|
+
if (state.state === "signed-in")
|
|
5505
|
+
return {
|
|
5506
|
+
maxSeconds: state.me.limits.maxSeconds,
|
|
5507
|
+
watermarkAfterSeconds: null
|
|
5508
|
+
};
|
|
5509
|
+
return {
|
|
5510
|
+
maxSeconds: DEFAULT_TUTTICODE_LIMITS.anonMaxSeconds,
|
|
5511
|
+
watermarkAfterSeconds: DEFAULT_TUTTICODE_LIMITS.anonWatermarkAfterSeconds
|
|
5512
|
+
};
|
|
5513
|
+
}
|
|
5514
|
+
var LOGIN_HINT = {
|
|
5515
|
+
command: TUTTICODE_LOGIN_HINT_COMMAND,
|
|
5516
|
+
note: "A free account lifts the limits. Run the command, ask the user to open the link it prints and approve the code; keep working meanwhile."
|
|
5517
|
+
};
|
|
5518
|
+
function signedInAs(me) {
|
|
5519
|
+
return `${me.user.email} (TuttiCode, ${me.plan.toLowerCase()})`;
|
|
5520
|
+
}
|
|
5521
|
+
async function revokeToken(origin, token, deps = {}) {
|
|
5522
|
+
try {
|
|
5523
|
+
const response = await request(
|
|
5524
|
+
origin,
|
|
5525
|
+
TUTTICODE_OAUTH_PATHS.revoke,
|
|
5526
|
+
{
|
|
5527
|
+
method: "POST",
|
|
5528
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5529
|
+
body: new URLSearchParams({
|
|
5530
|
+
token,
|
|
5531
|
+
client_id: TUTTICODE_CLIENT_ID
|
|
5532
|
+
}).toString()
|
|
5533
|
+
},
|
|
5534
|
+
deps
|
|
5535
|
+
);
|
|
5536
|
+
return response.status < 500;
|
|
5537
|
+
} catch {
|
|
5538
|
+
return false;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
|
|
4090
5542
|
// src/args.ts
|
|
4091
5543
|
var import_node_util = require("node:util");
|
|
4092
5544
|
|
|
4093
5545
|
// src/output.ts
|
|
4094
|
-
var
|
|
4095
|
-
var
|
|
5546
|
+
var import_node_fs3 = require("node:fs");
|
|
5547
|
+
var import_node_path3 = require("node:path");
|
|
4096
5548
|
var EXIT_OK = 0;
|
|
4097
5549
|
var EXIT_CODE_ERRORS = 1;
|
|
4098
5550
|
var EXIT_BRIDGE = 2;
|
|
4099
5551
|
var EXIT_USAGE = 3;
|
|
5552
|
+
var EXIT_ANONYMOUS = 4;
|
|
5553
|
+
var EXIT_LIMIT = 5;
|
|
5554
|
+
var EXIT_UPGRADE = 6;
|
|
4100
5555
|
var CODE_ERRORS = /* @__PURE__ */ new Set(["compile", "too-long", "clip"]);
|
|
4101
5556
|
function exitCodeFor(outcome) {
|
|
4102
5557
|
switch (outcome.kind) {
|
|
4103
5558
|
case "checked":
|
|
4104
5559
|
return outcome.report.ok ? EXIT_OK : EXIT_CODE_ERRORS;
|
|
4105
5560
|
case "error":
|
|
5561
|
+
if (outcome.code === "limit")
|
|
5562
|
+
return EXIT_LIMIT;
|
|
5563
|
+
if (outcome.code === "upgrade")
|
|
5564
|
+
return EXIT_UPGRADE;
|
|
4106
5565
|
return CODE_ERRORS.has(outcome.code) ? EXIT_CODE_ERRORS : EXIT_BRIDGE;
|
|
4107
5566
|
default:
|
|
4108
5567
|
return EXIT_OK;
|
|
@@ -4117,29 +5576,29 @@ function safeFileStem(label) {
|
|
|
4117
5576
|
return RESERVED_NAMES.test(cleaned) ? `${cleaned}_` : cleaned;
|
|
4118
5577
|
}
|
|
4119
5578
|
function formatFromPath(path) {
|
|
4120
|
-
const ext = (0,
|
|
5579
|
+
const ext = (0, import_node_path3.extname)(path).slice(1).toLowerCase();
|
|
4121
5580
|
return ext === "wav" || ext === "flac" || ext === "mp3" ? ext : void 0;
|
|
4122
5581
|
}
|
|
4123
5582
|
var FORMATS2 = ["wav", "flac", "mp3"];
|
|
4124
5583
|
function writeFileNoFollow(path, bytes) {
|
|
4125
5584
|
try {
|
|
4126
|
-
if ((0,
|
|
5585
|
+
if ((0, import_node_fs3.lstatSync)(path).isSymbolicLink())
|
|
4127
5586
|
throw new Error(`Refusing to write through the symbolic link ${path}.`);
|
|
4128
5587
|
} catch (e) {
|
|
4129
5588
|
if (e.code !== "ENOENT")
|
|
4130
5589
|
throw e;
|
|
4131
5590
|
}
|
|
4132
|
-
const fd = (0,
|
|
5591
|
+
const fd = (0, import_node_fs3.openSync)(
|
|
4133
5592
|
path,
|
|
4134
|
-
|
|
5593
|
+
import_node_fs3.constants.O_WRONLY | import_node_fs3.constants.O_CREAT | import_node_fs3.constants.O_TRUNC | // POSIX only (undefined on Windows): fail rather than follow a link
|
|
4135
5594
|
// swapped in after the check above.
|
|
4136
|
-
(
|
|
5595
|
+
(import_node_fs3.constants.O_NOFOLLOW ?? 0)
|
|
4137
5596
|
);
|
|
4138
5597
|
try {
|
|
4139
5598
|
for (let at = 0; at < bytes.length; )
|
|
4140
|
-
at += (0,
|
|
5599
|
+
at += (0, import_node_fs3.writeSync)(fd, bytes, at, bytes.length - at);
|
|
4141
5600
|
} finally {
|
|
4142
|
-
(0,
|
|
5601
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
4143
5602
|
}
|
|
4144
5603
|
}
|
|
4145
5604
|
function writeRender(meta, parts, out, stemsDir) {
|
|
@@ -4150,30 +5609,30 @@ function writeRender(meta, parts, out, stemsDir) {
|
|
|
4150
5609
|
const mix = parts.get(MIX_PART);
|
|
4151
5610
|
if (!mix)
|
|
4152
5611
|
throw new Error("The render has no mix.");
|
|
4153
|
-
const mixPath = (0,
|
|
4154
|
-
(0,
|
|
5612
|
+
const mixPath = (0, import_node_path3.resolve)(out);
|
|
5613
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(mixPath), { recursive: true });
|
|
4155
5614
|
writeFileNoFollow(mixPath, mix);
|
|
4156
5615
|
const stems = {};
|
|
4157
5616
|
if (meta.stems.length) {
|
|
4158
|
-
const requested = (0,
|
|
4159
|
-
stemsDir ?? mixPath.slice(0, mixPath.length - (0,
|
|
5617
|
+
const requested = (0, import_node_path3.resolve)(
|
|
5618
|
+
stemsDir ?? mixPath.slice(0, mixPath.length - (0, import_node_path3.extname)(mixPath).length) + ".stems"
|
|
4160
5619
|
);
|
|
4161
|
-
(0,
|
|
4162
|
-
const dir = (0,
|
|
5620
|
+
(0, import_node_fs3.mkdirSync)(requested, { recursive: true });
|
|
5621
|
+
const dir = (0, import_node_fs3.realpathSync)(requested);
|
|
4163
5622
|
const used = new Set(
|
|
4164
|
-
[requested, dir].includes((0,
|
|
5623
|
+
[requested, dir].includes((0, import_node_path3.dirname)(mixPath)) ? [(0, import_node_path3.basename)(mixPath).toLowerCase()] : []
|
|
4165
5624
|
);
|
|
4166
5625
|
for (const label of meta.stems) {
|
|
4167
5626
|
const bytes = parts.get(stemPart(label));
|
|
4168
5627
|
if (!bytes)
|
|
4169
5628
|
continue;
|
|
4170
5629
|
const base = safeFileStem(label);
|
|
4171
|
-
let
|
|
4172
|
-
for (let n = 2; used.has(
|
|
4173
|
-
|
|
4174
|
-
used.add(
|
|
4175
|
-
const path = (0,
|
|
4176
|
-
if ((0,
|
|
5630
|
+
let file2 = `${base}.${meta.format}`;
|
|
5631
|
+
for (let n = 2; used.has(file2.toLowerCase()); n++)
|
|
5632
|
+
file2 = `${base}-${n}.${meta.format}`;
|
|
5633
|
+
used.add(file2.toLowerCase());
|
|
5634
|
+
const path = (0, import_node_path3.resolve)(dir, file2);
|
|
5635
|
+
if ((0, import_node_path3.dirname)(path) !== dir || (0, import_node_path3.basename)(path) !== file2)
|
|
4177
5636
|
throw new Error(`Refusing to write a stem outside ${dir}.`);
|
|
4178
5637
|
writeFileNoFollow(path, bytes);
|
|
4179
5638
|
stems[label] = path;
|
|
@@ -4182,11 +5641,11 @@ function writeRender(meta, parts, out, stemsDir) {
|
|
|
4182
5641
|
return { mix: mixPath, stems };
|
|
4183
5642
|
}
|
|
4184
5643
|
var fixed = (n, digits = 1) => Number.isFinite(n) ? n.toFixed(digits) : String(n);
|
|
4185
|
-
function formatDiagnostics(report,
|
|
5644
|
+
function formatDiagnostics(report, file2) {
|
|
4186
5645
|
const lines = [];
|
|
4187
5646
|
for (const d of report.diagnostics) {
|
|
4188
5647
|
lines.push(
|
|
4189
|
-
`${
|
|
5648
|
+
`${file2}:${d.line}:${d.col} ${d.severity} ${d.code} ${d.message}`
|
|
4190
5649
|
);
|
|
4191
5650
|
if (d.snippet)
|
|
4192
5651
|
lines.push(` ${d.snippet}`);
|
|
@@ -4207,94 +5666,52 @@ function formatCheckSummary(report) {
|
|
|
4207
5666
|
)} s)`;
|
|
4208
5667
|
}
|
|
4209
5668
|
function formatRenderSummary(meta, files) {
|
|
5669
|
+
const gain = meta.gainDb ? `, gain ${meta.gainDb > 0 ? "+" : ""}${fixed(meta.gainDb)} dB` : "";
|
|
5670
|
+
const limited = meta.limitDb ? `, limited up to ${fixed(meta.limitDb)} dB` : "";
|
|
4210
5671
|
const lines = [
|
|
4211
5672
|
`Wrote ${files.mix} (${fixed(meta.seconds)} s, ${meta.bars} bars at ${fixed(
|
|
4212
5673
|
meta.bpm,
|
|
4213
5674
|
0
|
|
4214
|
-
)} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(
|
|
5675
|
+
)} BPM, ${fixed(meta.lufs)} LUFS, ${fixed(
|
|
5676
|
+
meta.truePeak
|
|
5677
|
+
)} dBTP${gain}${limited})`
|
|
4215
5678
|
];
|
|
5679
|
+
for (const warning of meta.warnings ?? [])
|
|
5680
|
+
lines.push(`Note: ${warning}`);
|
|
4216
5681
|
const stems = Object.values(files.stems);
|
|
4217
5682
|
if (stems.length)
|
|
4218
|
-
lines.push(`Stems: ${stems.length} files in ${(0,
|
|
5683
|
+
lines.push(`Stems: ${stems.length} files in ${(0, import_node_path3.dirname)(stems[0])}`);
|
|
4219
5684
|
lines.push(`Edit in TuttiDAW: ${meta.editUrl}`);
|
|
4220
5685
|
return lines;
|
|
4221
5686
|
}
|
|
5687
|
+
var WATERMARK_LINE = "Voice tag 'register at TuttiDAW' mixed in throughout this render (renders over 30 s without sign-in); the stats include it. Remove it: tutticode login (free)";
|
|
4222
5688
|
|
|
4223
5689
|
// src/server.ts
|
|
4224
|
-
var
|
|
5690
|
+
var import_node_crypto2 = require("node:crypto");
|
|
4225
5691
|
var import_node_http = require("node:http");
|
|
4226
5692
|
|
|
4227
5693
|
// ../../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
|
|
4228
5694
|
var import_stream = __toESM(require_stream(), 1);
|
|
4229
5695
|
var import_extension = __toESM(require_extension(), 1);
|
|
4230
5696
|
var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
4231
|
-
var import_receiver = __toESM(require_receiver(), 1);
|
|
4232
|
-
var import_sender = __toESM(require_sender(), 1);
|
|
4233
|
-
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
4234
|
-
var import_websocket = __toESM(require_websocket(), 1);
|
|
4235
|
-
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
4236
|
-
var wrapper_default = import_websocket.default;
|
|
4237
|
-
|
|
4238
|
-
// src/env.ts
|
|
4239
|
-
var import_node_fs2 = require("node:fs");
|
|
4240
|
-
var import_node_os = require("node:os");
|
|
4241
|
-
var import_node_path2 = require("node:path");
|
|
4242
|
-
var VERSION = true ? "0.1.0" : readPackageVersion();
|
|
4243
|
-
function envBrowserArgs() {
|
|
4244
|
-
const args = process.env.TUTTICODE_BROWSER_ARGS?.split(" ").filter(Boolean);
|
|
4245
|
-
return args?.length ? args : void 0;
|
|
4246
|
-
}
|
|
4247
|
-
function stateDir() {
|
|
4248
|
-
const dir = process.env.TUTTICODE_HOME || (0, import_node_path2.join)((0, import_node_os.homedir)(), ".tutticode");
|
|
4249
|
-
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
4250
|
-
return dir;
|
|
4251
|
-
}
|
|
4252
|
-
var assetsDir;
|
|
4253
|
-
function skillsDir() {
|
|
4254
|
-
return process.env.TUTTICODE_SKILLS_DIR || assetsDir || // Running from the sources (tests): the app's public skills.
|
|
4255
|
-
(0, import_node_path2.join)(__dirname, "..", "..", "..", "public", "skills");
|
|
4256
|
-
}
|
|
4257
|
-
function setSkillsDir(dir) {
|
|
4258
|
-
assetsDir = dir;
|
|
4259
|
-
}
|
|
4260
|
-
var LOG_ROLL_BYTES = 2 * 1024 * 1024;
|
|
4261
|
-
function createLogger(name, verbose) {
|
|
4262
|
-
let file;
|
|
4263
|
-
try {
|
|
4264
|
-
const dir = (0, import_node_path2.join)(stateDir(), "logs");
|
|
4265
|
-
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
4266
|
-
file = (0, import_node_path2.join)(dir, `${name}.log`);
|
|
4267
|
-
if (((0, import_node_fs2.statSync)(file, { throwIfNoEntry: false })?.size ?? 0) > LOG_ROLL_BYTES)
|
|
4268
|
-
(0, import_node_fs2.renameSync)(file, `${file}.1`);
|
|
4269
|
-
} catch {
|
|
4270
|
-
file = void 0;
|
|
4271
|
-
}
|
|
4272
|
-
return (level2, message) => {
|
|
4273
|
-
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level2} [${process.pid}] ${message}`;
|
|
4274
|
-
if (verbose || level2 === "error")
|
|
4275
|
-
process.stderr.write(`tutticode: ${message}
|
|
4276
|
-
`);
|
|
4277
|
-
if (!file)
|
|
4278
|
-
return;
|
|
4279
|
-
try {
|
|
4280
|
-
(0, import_node_fs2.appendFileSync)(file, `${line}
|
|
4281
|
-
`);
|
|
4282
|
-
} catch {
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4285
|
-
}
|
|
5697
|
+
var import_receiver = __toESM(require_receiver(), 1);
|
|
5698
|
+
var import_sender = __toESM(require_sender(), 1);
|
|
5699
|
+
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
5700
|
+
var import_websocket = __toESM(require_websocket(), 1);
|
|
5701
|
+
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
5702
|
+
var wrapper_default = import_websocket.default;
|
|
4286
5703
|
|
|
4287
5704
|
// src/server.ts
|
|
4288
5705
|
var CONTROL_PATH = "/control";
|
|
4289
5706
|
function newToken() {
|
|
4290
|
-
return (0,
|
|
5707
|
+
return (0, import_node_crypto2.randomBytes)(16).toString("base64url");
|
|
4291
5708
|
}
|
|
4292
5709
|
function tokensEqual(expected, presented) {
|
|
4293
5710
|
if (typeof presented !== "string")
|
|
4294
5711
|
return false;
|
|
4295
5712
|
const a = Buffer.from(expected);
|
|
4296
5713
|
const b = Buffer.from(presented);
|
|
4297
|
-
return a.length === b.length && (0,
|
|
5714
|
+
return a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
4298
5715
|
}
|
|
4299
5716
|
function normalizeOrigin(value) {
|
|
4300
5717
|
try {
|
|
@@ -4386,7 +5803,7 @@ var BridgeServer = class {
|
|
|
4386
5803
|
return res.end(
|
|
4387
5804
|
JSON.stringify({
|
|
4388
5805
|
name: "tutticode",
|
|
4389
|
-
version:
|
|
5806
|
+
version: VERSION2,
|
|
4390
5807
|
protocol: BRIDGE_PROTOCOL
|
|
4391
5808
|
})
|
|
4392
5809
|
);
|
|
@@ -4450,6 +5867,21 @@ function pathOf(req) {
|
|
|
4450
5867
|
// src/args.ts
|
|
4451
5868
|
var UsageError = class extends Error {
|
|
4452
5869
|
};
|
|
5870
|
+
var accountOptions = {
|
|
5871
|
+
origin: { type: "string" },
|
|
5872
|
+
locale: { type: "string" },
|
|
5873
|
+
verbose: { type: "boolean", short: "v" }
|
|
5874
|
+
};
|
|
5875
|
+
function accountFlags(values) {
|
|
5876
|
+
const origin = values.origin;
|
|
5877
|
+
if (origin !== void 0 && !normalizeOrigin(origin))
|
|
5878
|
+
throw new UsageError(`--origin must be an http(s) origin (got ${origin}).`);
|
|
5879
|
+
return {
|
|
5880
|
+
origin,
|
|
5881
|
+
locale: values.locale,
|
|
5882
|
+
verbose: !!values.verbose
|
|
5883
|
+
};
|
|
5884
|
+
}
|
|
4453
5885
|
var bridgeOptions = {
|
|
4454
5886
|
origin: { type: "string" },
|
|
4455
5887
|
"dev-origin": { type: "string", multiple: true },
|
|
@@ -4576,7 +6008,8 @@ function parseCli(argv) {
|
|
|
4576
6008
|
case "doctor": {
|
|
4577
6009
|
const { values, positionals } = parse(rest, {
|
|
4578
6010
|
...bridgeOptions,
|
|
4579
|
-
json: { type: "boolean" }
|
|
6011
|
+
json: { type: "boolean" },
|
|
6012
|
+
fix: { type: "boolean" }
|
|
4580
6013
|
});
|
|
4581
6014
|
if (values.help)
|
|
4582
6015
|
return { name: "help", topic: "doctor" };
|
|
@@ -4585,9 +6018,56 @@ function parseCli(argv) {
|
|
|
4585
6018
|
return {
|
|
4586
6019
|
name: "doctor",
|
|
4587
6020
|
json: !!values.json,
|
|
6021
|
+
fix: !!values.fix,
|
|
4588
6022
|
bridge: bridgeFlags(values)
|
|
4589
6023
|
};
|
|
4590
6024
|
}
|
|
6025
|
+
case "login": {
|
|
6026
|
+
const { values, positionals } = parse(rest, {
|
|
6027
|
+
...accountOptions,
|
|
6028
|
+
device: { type: "boolean" },
|
|
6029
|
+
"no-wait": { type: "boolean" },
|
|
6030
|
+
json: { type: "boolean" }
|
|
6031
|
+
});
|
|
6032
|
+
if (values.help)
|
|
6033
|
+
return { name: "help", topic: "login" };
|
|
6034
|
+
if (positionals.length)
|
|
6035
|
+
throw new UsageError("login takes no arguments.");
|
|
6036
|
+
const noWait = !!values["no-wait"];
|
|
6037
|
+
if (values.json && !noWait)
|
|
6038
|
+
throw new UsageError("--json goes with --device --no-wait.");
|
|
6039
|
+
return {
|
|
6040
|
+
name: "login",
|
|
6041
|
+
// Only the device grant can finish without this process.
|
|
6042
|
+
device: !!values.device || noWait,
|
|
6043
|
+
noWait,
|
|
6044
|
+
json: !!values.json,
|
|
6045
|
+
account: accountFlags(values)
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
6048
|
+
case "logout": {
|
|
6049
|
+
const { values, positionals } = parse(rest, accountOptions);
|
|
6050
|
+
if (values.help)
|
|
6051
|
+
return { name: "help", topic: "logout" };
|
|
6052
|
+
if (positionals.length)
|
|
6053
|
+
throw new UsageError("logout takes no arguments.");
|
|
6054
|
+
return { name: "logout", account: accountFlags(values) };
|
|
6055
|
+
}
|
|
6056
|
+
case "whoami": {
|
|
6057
|
+
const { values, positionals } = parse(rest, {
|
|
6058
|
+
...accountOptions,
|
|
6059
|
+
json: { type: "boolean" }
|
|
6060
|
+
});
|
|
6061
|
+
if (values.help)
|
|
6062
|
+
return { name: "help", topic: "whoami" };
|
|
6063
|
+
if (positionals.length)
|
|
6064
|
+
throw new UsageError("whoami takes no arguments.");
|
|
6065
|
+
return {
|
|
6066
|
+
name: "whoami",
|
|
6067
|
+
json: !!values.json,
|
|
6068
|
+
account: accountFlags(values)
|
|
6069
|
+
};
|
|
6070
|
+
}
|
|
4591
6071
|
case "setup": {
|
|
4592
6072
|
const { values, positionals } = parse(rest, {
|
|
4593
6073
|
yes: { type: "boolean", short: "y" },
|
|
@@ -4632,6 +6112,8 @@ function parseCli(argv) {
|
|
|
4632
6112
|
}
|
|
4633
6113
|
case "__daemon":
|
|
4634
6114
|
return { name: "daemon", config: rest[0] ?? "" };
|
|
6115
|
+
case "__login-poll":
|
|
6116
|
+
return { name: "login-poll", file: rest[0] ?? "" };
|
|
4635
6117
|
default:
|
|
4636
6118
|
throw new UsageError(`Unknown command ${name}. Run tutticode --help.`);
|
|
4637
6119
|
}
|
|
@@ -4653,12 +6135,14 @@ function parseRender(rest) {
|
|
|
4653
6135
|
tail: { type: "string" },
|
|
4654
6136
|
lufs: { type: "string" },
|
|
4655
6137
|
"true-peak": { type: "string" },
|
|
6138
|
+
limit: { type: "boolean" },
|
|
4656
6139
|
"no-analyze": { type: "boolean" },
|
|
6140
|
+
"no-brand-tag": { type: "boolean" },
|
|
4657
6141
|
json: { type: "boolean" }
|
|
4658
6142
|
});
|
|
4659
6143
|
if (values.help)
|
|
4660
6144
|
return { name: "help", topic: "render" };
|
|
4661
|
-
const
|
|
6145
|
+
const file2 = oneFile(positionals, "render");
|
|
4662
6146
|
const out = values.out;
|
|
4663
6147
|
if (!out)
|
|
4664
6148
|
throw new UsageError(
|
|
@@ -4707,11 +6191,13 @@ function parseRender(rest) {
|
|
|
4707
6191
|
...optional2("tail", number(values, "tail", 0, 60)),
|
|
4708
6192
|
...optional2("lufs", number(values, "lufs", -40, -5)),
|
|
4709
6193
|
...optional2("truePeak", number(values, "true-peak", -12, 0)),
|
|
4710
|
-
...values
|
|
6194
|
+
...values.limit ? { limit: true } : {},
|
|
6195
|
+
...values["no-analyze"] ? { analyze: false } : {},
|
|
6196
|
+
...values["no-brand-tag"] ? { brandTag: false } : {}
|
|
4711
6197
|
};
|
|
4712
6198
|
return {
|
|
4713
6199
|
name: "render",
|
|
4714
|
-
file,
|
|
6200
|
+
file: file2,
|
|
4715
6201
|
out,
|
|
4716
6202
|
stemsDir: values.stems,
|
|
4717
6203
|
json: !!values.json,
|
|
@@ -4731,10 +6217,19 @@ Usage:
|
|
|
4731
6217
|
tutticode open <file|-> open the program in TuttiCode
|
|
4732
6218
|
tutticode mcp MCP stdio server (code_check, code_render, ...)
|
|
4733
6219
|
tutticode doctor is everything in place?
|
|
6220
|
+
tutticode login sign in (free): longer renders, no voice tag
|
|
6221
|
+
tutticode logout sign out on this computer
|
|
6222
|
+
tutticode whoami who renders, with which limits
|
|
4734
6223
|
tutticode setup optional browser policy + skills
|
|
4735
6224
|
tutticode skills install install the agent skills
|
|
4736
6225
|
|
|
4737
|
-
|
|
6226
|
+
Once TuttiDAW switches its limits on (being rolled out), renders without
|
|
6227
|
+
sign-in are limited to 60 s, and renders over 30 s carry a spoken "register
|
|
6228
|
+
at TuttiDAW" tag. TUTTICODE_TOKEN (an access token from the
|
|
6229
|
+
account page) signs in CI.
|
|
6230
|
+
|
|
6231
|
+
Exit codes: 0 ok, 1 errors in the code, 2 bridge/browser problem, 3 usage,
|
|
6232
|
+
4 not signed in (whoami), 5 over a limit without sign-in, 6 upgrade tutticode.
|
|
4738
6233
|
Run tutticode <command> --help for its flags.`,
|
|
4739
6234
|
check: `tutticode check <file|-> [--bpm N] [--json] [bridge flags]
|
|
4740
6235
|
|
|
@@ -4751,21 +6246,52 @@ program has errors.`,
|
|
|
4751
6246
|
--stems <dir> also write one file per voice into <dir>
|
|
4752
6247
|
--fade-in S --fade-out S --tail S seconds
|
|
4753
6248
|
--lufs N --true-peak N static gain to a loudness; fails (clip)
|
|
4754
|
-
|
|
6249
|
+
when the true peak would pass the
|
|
6250
|
+
ceiling (default -1 dBTP). Without
|
|
6251
|
+
--lufs a render over the ceiling is
|
|
6252
|
+
lowered (one static gain, reported)
|
|
6253
|
+
--limit true-peak limiter instead of failing
|
|
6254
|
+
(clip) or lowering; reported
|
|
4755
6255
|
--no-analyze skip the per-voice analysis
|
|
6256
|
+
--no-brand-tag no "Made with TuttiCode" file metadata
|
|
6257
|
+
(signed in only; TUTTICODE_BRAND_TAG=0)
|
|
4756
6258
|
--json print the RenderMeta + written files
|
|
4757
6259
|
--timeout S give up after S seconds (default 600)`,
|
|
4758
6260
|
open: `tutticode open <file|-> [bridge flags]
|
|
4759
6261
|
|
|
4760
|
-
Opens the program in a visible TuttiCode tab (tuttidaw.com/code)
|
|
6262
|
+
Opens the program in a visible TuttiCode tab (tuttidaw.com/code). With
|
|
6263
|
+
--headless it opens nothing and prints the link.`,
|
|
4761
6264
|
mcp: `tutticode mcp [bridge flags]
|
|
4762
6265
|
|
|
4763
6266
|
MCP stdio server: code_check, code_render, code_compare, code_open,
|
|
4764
|
-
code_reference; resources = the TuttiCode skill references.
|
|
6267
|
+
code_reference, code_account; resources = the TuttiCode skill references.
|
|
6268
|
+
Signs the page in like the CLI (tutticode login, or TUTTICODE_TOKEN).
|
|
4765
6269
|
claude mcp add tutticode -- tutticode mcp`,
|
|
4766
|
-
doctor: `tutticode doctor [--json] [bridge flags]
|
|
6270
|
+
doctor: `tutticode doctor [--json] [--fix] [bridge flags]
|
|
4767
6271
|
|
|
4768
|
-
Checks the browser, the page and the connection,
|
|
6272
|
+
Checks the browser, the page and the connection, prints the engine version
|
|
6273
|
+
and who renders (signed in / anonymous, the credentials file's permissions).
|
|
6274
|
+
--fix makes the credentials file private again.`,
|
|
6275
|
+
login: `tutticode login [--device [--no-wait] [--json]] [--origin <url>] [--locale <l>]
|
|
6276
|
+
|
|
6277
|
+
Signs in with a free TuttiDAW account: renders up to 5 minutes, no voice tag.
|
|
6278
|
+
Opens your browser; approve there and come back. The sign-in can only render
|
|
6279
|
+
with TuttiCode (it cannot open your projects, account or billing) and lasts a
|
|
6280
|
+
year.
|
|
6281
|
+
--device no browser here (SSH, a server): prints a code to approve at
|
|
6282
|
+
tuttidaw.com/code/activate on any device
|
|
6283
|
+
--no-wait with --device: print the code and return at once; tutticode
|
|
6284
|
+
finishes the sign-in in the background once it is approved.
|
|
6285
|
+
For agents: tutticode login --device --no-wait --json
|
|
6286
|
+
Needs a terminal unless --no-wait.`,
|
|
6287
|
+
logout: `tutticode logout [--origin <url>]
|
|
6288
|
+
|
|
6289
|
+
Signs this computer out (revokes its sign-in) and deletes the stored
|
|
6290
|
+
credentials. Other devices and access tokens: the account page.`,
|
|
6291
|
+
whoami: `tutticode whoami [--json] [--origin <url>]
|
|
6292
|
+
|
|
6293
|
+
Who renders: the masked email, plan, limits and today's usage. Exit 0 signed
|
|
6294
|
+
in, 4 not signed in, 2 when the sign-in was rejected.`,
|
|
4769
6295
|
setup: `tutticode setup [--yes] [--undo] [--no-skills]
|
|
4770
6296
|
|
|
4771
6297
|
Optionally lets tuttidaw.com reach tutticode in your everyday Chrome / Edge
|
|
@@ -4787,10 +6313,10 @@ agent found under your home directory).`,
|
|
|
4787
6313
|
};
|
|
4788
6314
|
|
|
4789
6315
|
// src/browser.ts
|
|
4790
|
-
var
|
|
4791
|
-
var
|
|
4792
|
-
var
|
|
4793
|
-
var
|
|
6316
|
+
var import_node_child_process2 = require("node:child_process");
|
|
6317
|
+
var import_node_fs4 = require("node:fs");
|
|
6318
|
+
var import_node_os3 = require("node:os");
|
|
6319
|
+
var import_node_path4 = require("node:path");
|
|
4794
6320
|
|
|
4795
6321
|
// src/cdp.ts
|
|
4796
6322
|
var import_node_events = require("node:events");
|
|
@@ -4895,8 +6421,8 @@ var CdpClient = class extends import_node_events.EventEmitter {
|
|
|
4895
6421
|
var hostPlatform = () => ({
|
|
4896
6422
|
platform: process.platform,
|
|
4897
6423
|
env: process.env,
|
|
4898
|
-
home: (0,
|
|
4899
|
-
exists:
|
|
6424
|
+
home: (0, import_node_os3.homedir)(),
|
|
6425
|
+
exists: import_node_fs4.existsSync
|
|
4900
6426
|
});
|
|
4901
6427
|
function browserCandidates(p = hostPlatform()) {
|
|
4902
6428
|
if (p.platform === "win32") {
|
|
@@ -4908,20 +6434,20 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4908
6434
|
return [
|
|
4909
6435
|
...roots.map((r2) => ({
|
|
4910
6436
|
kind: "chrome",
|
|
4911
|
-
path: (0,
|
|
6437
|
+
path: (0, import_node_path4.join)(r2, "Google", "Chrome", "Application", "chrome.exe")
|
|
4912
6438
|
})),
|
|
4913
6439
|
...roots.map((r2) => ({
|
|
4914
6440
|
kind: "edge",
|
|
4915
|
-
path: (0,
|
|
6441
|
+
path: (0, import_node_path4.join)(r2, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
4916
6442
|
}))
|
|
4917
6443
|
];
|
|
4918
6444
|
}
|
|
4919
6445
|
if (p.platform === "darwin") {
|
|
4920
|
-
const apps = ["/Applications", (0,
|
|
6446
|
+
const apps = ["/Applications", (0, import_node_path4.join)(p.home, "Applications")];
|
|
4921
6447
|
return [
|
|
4922
6448
|
...apps.map((a) => ({
|
|
4923
6449
|
kind: "chrome",
|
|
4924
|
-
path: (0,
|
|
6450
|
+
path: (0, import_node_path4.join)(
|
|
4925
6451
|
a,
|
|
4926
6452
|
"Google Chrome.app",
|
|
4927
6453
|
"Contents",
|
|
@@ -4931,7 +6457,7 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4931
6457
|
})),
|
|
4932
6458
|
...apps.map((a) => ({
|
|
4933
6459
|
kind: "edge",
|
|
4934
|
-
path: (0,
|
|
6460
|
+
path: (0, import_node_path4.join)(
|
|
4935
6461
|
a,
|
|
4936
6462
|
"Microsoft Edge.app",
|
|
4937
6463
|
"Contents",
|
|
@@ -4941,7 +6467,7 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4941
6467
|
})),
|
|
4942
6468
|
...apps.map((a) => ({
|
|
4943
6469
|
kind: "chromium",
|
|
4944
|
-
path: (0,
|
|
6470
|
+
path: (0, import_node_path4.join)(a, "Chromium.app", "Contents", "MacOS", "Chromium")
|
|
4945
6471
|
}))
|
|
4946
6472
|
];
|
|
4947
6473
|
}
|
|
@@ -4955,7 +6481,7 @@ function browserCandidates(p = hostPlatform()) {
|
|
|
4955
6481
|
["chromium", "chromium-browser"]
|
|
4956
6482
|
];
|
|
4957
6483
|
return names.flatMap(
|
|
4958
|
-
([kind, name]) => dirs.map((d) => ({ kind, path: (0,
|
|
6484
|
+
([kind, name]) => dirs.map((d) => ({ kind, path: (0, import_node_path4.join)(d, name) }))
|
|
4959
6485
|
);
|
|
4960
6486
|
}
|
|
4961
6487
|
function findBrowser(wanted, p = hostPlatform()) {
|
|
@@ -4974,7 +6500,7 @@ function findBrowser(wanted, p = hostPlatform()) {
|
|
|
4974
6500
|
}
|
|
4975
6501
|
function openExternal(url) {
|
|
4976
6502
|
const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
4977
|
-
const child = (0,
|
|
6503
|
+
const child = (0, import_node_child_process2.spawn)(command, args, {
|
|
4978
6504
|
detached: true,
|
|
4979
6505
|
stdio: "ignore",
|
|
4980
6506
|
windowsHide: true
|
|
@@ -5076,7 +6602,7 @@ var HeadlessBrowser = class {
|
|
|
5076
6602
|
}
|
|
5077
6603
|
}
|
|
5078
6604
|
if (this.tempProfile) {
|
|
5079
|
-
(0,
|
|
6605
|
+
(0, import_node_fs4.rmSync)(this.tempProfile, { recursive: true, force: true });
|
|
5080
6606
|
this.tempProfile = void 0;
|
|
5081
6607
|
}
|
|
5082
6608
|
}
|
|
@@ -5084,8 +6610,8 @@ var HeadlessBrowser = class {
|
|
|
5084
6610
|
await this.close();
|
|
5085
6611
|
this.consoleLines = [];
|
|
5086
6612
|
const { browser, log } = this.options;
|
|
5087
|
-
const shared = this.options.profileDir ?? (0,
|
|
5088
|
-
(0,
|
|
6613
|
+
const shared = this.options.profileDir ?? (0, import_node_path4.join)(stateDir(), "browser-profile");
|
|
6614
|
+
(0, import_node_fs4.mkdirSync)(shared, { recursive: true });
|
|
5089
6615
|
try {
|
|
5090
6616
|
this.cdp = await this.spawnBrowser(shared);
|
|
5091
6617
|
} catch (e) {
|
|
@@ -5093,7 +6619,7 @@ var HeadlessBrowser = class {
|
|
|
5093
6619
|
"warn",
|
|
5094
6620
|
`shared browser profile unavailable (${e.message}); using a temporary one`
|
|
5095
6621
|
);
|
|
5096
|
-
this.tempProfile = (0,
|
|
6622
|
+
this.tempProfile = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "tutticode-profile-"));
|
|
5097
6623
|
this.cdp = await this.spawnBrowser(this.tempProfile);
|
|
5098
6624
|
}
|
|
5099
6625
|
this.cdp.on("event", (event) => this.onEvent(event));
|
|
@@ -5129,7 +6655,7 @@ var HeadlessBrowser = class {
|
|
|
5129
6655
|
"about:blank"
|
|
5130
6656
|
];
|
|
5131
6657
|
return new Promise((resolve3, reject) => {
|
|
5132
|
-
const child = (0,
|
|
6658
|
+
const child = (0, import_node_child_process2.spawn)(browser.path, args, {
|
|
5133
6659
|
// fd 3: commands to the browser, fd 4: its replies and events.
|
|
5134
6660
|
stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"],
|
|
5135
6661
|
windowsHide: true
|
|
@@ -5246,7 +6772,7 @@ function remoteDebuggingFree(args) {
|
|
|
5246
6772
|
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5247
6773
|
function killTree(child) {
|
|
5248
6774
|
if (process.platform === "win32" && child.pid)
|
|
5249
|
-
(0,
|
|
6775
|
+
(0, import_node_child_process2.spawnSync)("taskkill", ["/T", "/F", "/PID", String(child.pid)], {
|
|
5250
6776
|
windowsHide: true
|
|
5251
6777
|
});
|
|
5252
6778
|
else
|
|
@@ -5254,18 +6780,19 @@ function killTree(child) {
|
|
|
5254
6780
|
}
|
|
5255
6781
|
|
|
5256
6782
|
// src/daemon.ts
|
|
5257
|
-
var
|
|
5258
|
-
var
|
|
5259
|
-
var
|
|
5260
|
-
var
|
|
6783
|
+
var import_node_child_process3 = require("node:child_process");
|
|
6784
|
+
var import_node_crypto4 = require("node:crypto");
|
|
6785
|
+
var import_node_fs5 = require("node:fs");
|
|
6786
|
+
var import_node_path5 = require("node:path");
|
|
5261
6787
|
|
|
5262
6788
|
// src/session.ts
|
|
5263
|
-
var
|
|
6789
|
+
var import_node_crypto3 = require("node:crypto");
|
|
5264
6790
|
var import_node_events3 = require("node:events");
|
|
5265
6791
|
|
|
5266
6792
|
// src/page-link.ts
|
|
5267
6793
|
var import_node_events2 = require("node:events");
|
|
5268
6794
|
var HELLO_TIMEOUT_MS = 15e3;
|
|
6795
|
+
var AUTH_TIMEOUT_MS = 1e4;
|
|
5269
6796
|
var CANCEL_GRACE_MS = 5e3;
|
|
5270
6797
|
function maxPartBytes(meta) {
|
|
5271
6798
|
return Math.ceil((meta.seconds + 1) * meta.sampleRate) * 2 * 4 + 1024 * 1024;
|
|
@@ -5319,6 +6846,16 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5319
6846
|
drain;
|
|
5320
6847
|
closedReason;
|
|
5321
6848
|
readyPromise;
|
|
6849
|
+
/** The token ('' = anonymous) of the last `auth` the page took. */
|
|
6850
|
+
authKey;
|
|
6851
|
+
authStatus;
|
|
6852
|
+
/** The page refused `auth` itself: stop sending it. */
|
|
6853
|
+
authUnsupported = false;
|
|
6854
|
+
/** The page's last `error auth` message. */
|
|
6855
|
+
authError;
|
|
6856
|
+
/** Tokens the page refused -> its message. */
|
|
6857
|
+
refused = /* @__PURE__ */ new Map();
|
|
6858
|
+
authWait;
|
|
5322
6859
|
get open() {
|
|
5323
6860
|
return this.closedReason === void 0;
|
|
5324
6861
|
}
|
|
@@ -5330,6 +6867,73 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5330
6867
|
get busy() {
|
|
5331
6868
|
return !!this.current || !!this.drain;
|
|
5332
6869
|
}
|
|
6870
|
+
/**
|
|
6871
|
+
* Sends `auth` unless this socket already carries the same token, and
|
|
6872
|
+
* waits for the page's verdict. A token the page refuses is dropped: the
|
|
6873
|
+
* socket signs in anonymously and the status says why. Never rejects.
|
|
6874
|
+
*/
|
|
6875
|
+
async authenticate(auth, timeoutMs = AUTH_TIMEOUT_MS) {
|
|
6876
|
+
if (this.authUnsupported)
|
|
6877
|
+
return { signedIn: false, unsupported: true };
|
|
6878
|
+
const refused = auth.accessToken && this.refused.get(auth.accessToken);
|
|
6879
|
+
if (refused) {
|
|
6880
|
+
const { accessToken: _dropped2, ...anonymous2 } = auth;
|
|
6881
|
+
return {
|
|
6882
|
+
...await this.authenticate(anonymous2, timeoutMs),
|
|
6883
|
+
rejected: refused
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
const key = auth.accessToken ?? "";
|
|
6887
|
+
if (this.authKey === key && this.authStatus)
|
|
6888
|
+
return this.authStatus;
|
|
6889
|
+
this.authError = void 0;
|
|
6890
|
+
const answer = await this.sendAuth(auth, timeoutMs);
|
|
6891
|
+
if (answer !== "hello") {
|
|
6892
|
+
if (answer !== "closed") {
|
|
6893
|
+
this.authUnsupported = true;
|
|
6894
|
+
this.log(
|
|
6895
|
+
"warn",
|
|
6896
|
+
answer === "protocol" ? "the page does not know auth: rendering anonymously" : "the page did not answer auth: rendering anonymously"
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6899
|
+
return { signedIn: false, unsupported: true };
|
|
6900
|
+
}
|
|
6901
|
+
const signedIn = !!this.hello?.signedIn;
|
|
6902
|
+
if (!auth.accessToken || signedIn) {
|
|
6903
|
+
this.authKey = key;
|
|
6904
|
+
return this.authStatus = { signedIn };
|
|
6905
|
+
}
|
|
6906
|
+
const rejected = this.authError ?? "The page did not accept the sign-in.";
|
|
6907
|
+
this.log("warn", `page refused the sign-in: ${rejected}`);
|
|
6908
|
+
this.refused.set(auth.accessToken, rejected);
|
|
6909
|
+
const { accessToken: _dropped, ...anonymous } = auth;
|
|
6910
|
+
const again = await this.sendAuth(anonymous, timeoutMs);
|
|
6911
|
+
if (again === "hello") {
|
|
6912
|
+
this.authKey = "";
|
|
6913
|
+
this.authStatus = { signedIn: !!this.hello?.signedIn };
|
|
6914
|
+
}
|
|
6915
|
+
return { signedIn: again === "hello" && !!this.hello?.signedIn, rejected };
|
|
6916
|
+
}
|
|
6917
|
+
sendAuth(auth, timeoutMs) {
|
|
6918
|
+
if (!this.open)
|
|
6919
|
+
return Promise.resolve("closed");
|
|
6920
|
+
return new Promise((resolve3) => {
|
|
6921
|
+
const done = (answer) => {
|
|
6922
|
+
clearTimeout(timer);
|
|
6923
|
+
this.off("close", onClose);
|
|
6924
|
+
this.authWait = void 0;
|
|
6925
|
+
resolve3(answer);
|
|
6926
|
+
};
|
|
6927
|
+
const onClose = () => done("closed");
|
|
6928
|
+
const timer = setTimeout(() => done("timeout"), timeoutMs);
|
|
6929
|
+
this.once("close", onClose);
|
|
6930
|
+
this.authWait = (answer) => {
|
|
6931
|
+
if (answer !== "auth")
|
|
6932
|
+
done(answer);
|
|
6933
|
+
};
|
|
6934
|
+
this.send({ type: "auth", ...auth });
|
|
6935
|
+
});
|
|
6936
|
+
}
|
|
5333
6937
|
/** Runs one command. Never rejects: failures are an `error` outcome. */
|
|
5334
6938
|
async run(command, options) {
|
|
5335
6939
|
if (!this.open)
|
|
@@ -5445,8 +7049,13 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5445
7049
|
}
|
|
5446
7050
|
onMessage(message) {
|
|
5447
7051
|
if (message.type === "hello") {
|
|
5448
|
-
if (this.hello)
|
|
7052
|
+
if (this.hello) {
|
|
7053
|
+
if (message.protocol === BRIDGE_PROTOCOL) {
|
|
7054
|
+
this.hello = message;
|
|
7055
|
+
this.authWait?.("hello");
|
|
7056
|
+
}
|
|
5449
7057
|
return;
|
|
7058
|
+
}
|
|
5450
7059
|
if (message.protocol !== BRIDGE_PROTOCOL) {
|
|
5451
7060
|
const why = `The page speaks bridge protocol ${message.protocol}, this tutticode speaks ${BRIDGE_PROTOCOL}: update tutticode (npm i -g tutticode@latest).`;
|
|
5452
7061
|
this.emit("bad-hello", why);
|
|
@@ -5466,6 +7075,20 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5466
7075
|
return;
|
|
5467
7076
|
}
|
|
5468
7077
|
const running = this.current;
|
|
7078
|
+
if (message.type === "error" && id === void 0 && message.code === "auth") {
|
|
7079
|
+
this.log("warn", `page error auth: ${message.message}`);
|
|
7080
|
+
this.authError = message.message;
|
|
7081
|
+
if (this.authKey) {
|
|
7082
|
+
this.authKey = void 0;
|
|
7083
|
+
this.authStatus = void 0;
|
|
7084
|
+
}
|
|
7085
|
+
this.authWait?.("auth");
|
|
7086
|
+
return;
|
|
7087
|
+
}
|
|
7088
|
+
if (message.type === "error" && id === void 0 && this.authWait) {
|
|
7089
|
+
this.log("warn", `page error ${message.code}: ${message.message}`);
|
|
7090
|
+
return this.authWait("protocol");
|
|
7091
|
+
}
|
|
5469
7092
|
if (message.type === "error" && id === void 0) {
|
|
5470
7093
|
this.log("warn", `page error ${message.code}: ${message.message}`);
|
|
5471
7094
|
return this.fail(message.code, message.message, message.report);
|
|
@@ -5501,7 +7124,8 @@ var PageLink = class extends import_node_events2.EventEmitter {
|
|
|
5501
7124
|
kind: "error",
|
|
5502
7125
|
code: message.code,
|
|
5503
7126
|
message: message.message,
|
|
5504
|
-
...message.report ? { report: message.report } : {}
|
|
7127
|
+
...message.report ? { report: message.report } : {},
|
|
7128
|
+
...message.limit ? { limit: message.limit } : {}
|
|
5505
7129
|
});
|
|
5506
7130
|
case "done": {
|
|
5507
7131
|
if (!running.meta)
|
|
@@ -5633,7 +7257,13 @@ var Session = class extends import_node_events3.EventEmitter {
|
|
|
5633
7257
|
const job = this.queue.then(async () => {
|
|
5634
7258
|
try {
|
|
5635
7259
|
const link = await this.ensurePage(options.signal);
|
|
5636
|
-
|
|
7260
|
+
if (!options.auth)
|
|
7261
|
+
return await link.run(command, options);
|
|
7262
|
+
const auth = await link.authenticate(
|
|
7263
|
+
options.auth,
|
|
7264
|
+
this.options.authTimeoutMs
|
|
7265
|
+
);
|
|
7266
|
+
return { ...await link.run(command, options), auth };
|
|
5637
7267
|
} catch (e) {
|
|
5638
7268
|
if (e instanceof BridgeError)
|
|
5639
7269
|
return { kind: "error", code: e.code, message: e.message };
|
|
@@ -5781,14 +7411,14 @@ Page console:
|
|
|
5781
7411
|
// src/daemon.ts
|
|
5782
7412
|
function daemonKey(config) {
|
|
5783
7413
|
const { verbose: _verbose, ...identity } = config;
|
|
5784
|
-
return (0,
|
|
7414
|
+
return (0, import_node_crypto4.createHash)("sha256").update(
|
|
5785
7415
|
JSON.stringify({
|
|
5786
7416
|
...identity,
|
|
5787
7417
|
devOrigins: [...config.devOrigins ?? []].sort()
|
|
5788
7418
|
})
|
|
5789
7419
|
).digest("hex").slice(0, 12);
|
|
5790
7420
|
}
|
|
5791
|
-
var daemonFile = (key) => (0,
|
|
7421
|
+
var daemonFile = (key) => (0, import_node_path5.join)(stateDir(), `daemon-${key}.json`);
|
|
5792
7422
|
async function runDaemon(config, log) {
|
|
5793
7423
|
const controlToken = newToken();
|
|
5794
7424
|
let active = 0;
|
|
@@ -5816,22 +7446,22 @@ async function runDaemon(config, log) {
|
|
|
5816
7446
|
const session = new Session(options);
|
|
5817
7447
|
const port = await session.start();
|
|
5818
7448
|
const key = daemonKey(config);
|
|
5819
|
-
const
|
|
7449
|
+
const file2 = {
|
|
5820
7450
|
pid: process.pid,
|
|
5821
7451
|
port,
|
|
5822
7452
|
token: controlToken,
|
|
5823
|
-
version:
|
|
7453
|
+
version: VERSION2
|
|
5824
7454
|
};
|
|
5825
|
-
(0,
|
|
7455
|
+
(0, import_node_fs5.writeFileSync)(daemonFile(key), JSON.stringify(file2), { mode: 384 });
|
|
5826
7456
|
log("info", `daemon ${key} listening on 127.0.0.1:${port} (${config.mode})`);
|
|
5827
7457
|
const shutdown = async (why) => {
|
|
5828
7458
|
log("info", `daemon exiting: ${why}`);
|
|
5829
7459
|
try {
|
|
5830
7460
|
const current = JSON.parse(
|
|
5831
|
-
(0,
|
|
7461
|
+
(0, import_node_fs5.readFileSync)(daemonFile(key), "utf8")
|
|
5832
7462
|
);
|
|
5833
7463
|
if (current.pid === process.pid)
|
|
5834
|
-
(0,
|
|
7464
|
+
(0, import_node_fs5.rmSync)(daemonFile(key), { force: true });
|
|
5835
7465
|
} catch {
|
|
5836
7466
|
}
|
|
5837
7467
|
await session.close();
|
|
@@ -5852,14 +7482,14 @@ function serveControl(socket, session, setStatusSink) {
|
|
|
5852
7482
|
socket.send(JSON.stringify(value));
|
|
5853
7483
|
};
|
|
5854
7484
|
socket.once("message", async (data) => {
|
|
5855
|
-
let
|
|
7485
|
+
let request2;
|
|
5856
7486
|
try {
|
|
5857
|
-
|
|
7487
|
+
request2 = JSON.parse(String(data));
|
|
5858
7488
|
} catch {
|
|
5859
7489
|
return socket.close(1008, "bad request");
|
|
5860
7490
|
}
|
|
5861
7491
|
setStatusSink((message) => send({ type: "status", message }));
|
|
5862
|
-
if (
|
|
7492
|
+
if (request2.type === "info") {
|
|
5863
7493
|
try {
|
|
5864
7494
|
await session.ensurePage(abort.signal);
|
|
5865
7495
|
send({
|
|
@@ -5882,28 +7512,33 @@ function serveControl(socket, session, setStatusSink) {
|
|
|
5882
7512
|
}
|
|
5883
7513
|
return socket.close(1e3);
|
|
5884
7514
|
}
|
|
5885
|
-
if (
|
|
7515
|
+
if (request2.type !== "run" || !request2.command)
|
|
5886
7516
|
return socket.close(1008, "bad request");
|
|
5887
|
-
const outcome = await session.run(
|
|
5888
|
-
timeoutMs:
|
|
7517
|
+
const outcome = await session.run(request2.command, {
|
|
7518
|
+
timeoutMs: request2.timeoutMs ?? 6e5,
|
|
5889
7519
|
signal: abort.signal,
|
|
5890
|
-
onProgress: (progress) => send(progress)
|
|
7520
|
+
onProgress: (progress) => send(progress),
|
|
7521
|
+
...request2.auth ? { auth: request2.auth } : {}
|
|
5891
7522
|
});
|
|
5892
7523
|
if (outcome.kind === "rendered") {
|
|
5893
|
-
const id =
|
|
7524
|
+
const id = request2.command.type === "open" ? "open" : request2.command.id;
|
|
5894
7525
|
for (const [part, bytes] of outcome.parts)
|
|
5895
7526
|
for (const frame of frameParts(id, part, bytes))
|
|
5896
7527
|
socket.send(frame);
|
|
5897
7528
|
send({
|
|
5898
7529
|
type: "result",
|
|
5899
|
-
outcome: {
|
|
7530
|
+
outcome: {
|
|
7531
|
+
kind: "rendered",
|
|
7532
|
+
meta: outcome.meta,
|
|
7533
|
+
...outcome.auth ? { auth: outcome.auth } : {}
|
|
7534
|
+
}
|
|
5900
7535
|
});
|
|
5901
7536
|
} else
|
|
5902
7537
|
send({ type: "result", outcome });
|
|
5903
7538
|
socket.close(1e3);
|
|
5904
7539
|
});
|
|
5905
7540
|
}
|
|
5906
|
-
async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
|
|
7541
|
+
async function runViaDaemon(options, command, timeoutMs, callbacks = {}, auth) {
|
|
5907
7542
|
let socket;
|
|
5908
7543
|
try {
|
|
5909
7544
|
socket = await connectDaemon(options);
|
|
@@ -5912,7 +7547,7 @@ async function runViaDaemon(options, command, timeoutMs, callbacks = {}) {
|
|
|
5912
7547
|
}
|
|
5913
7548
|
return controlCall(
|
|
5914
7549
|
socket,
|
|
5915
|
-
{ type: "run", command, timeoutMs },
|
|
7550
|
+
{ type: "run", command, timeoutMs, ...auth ? { auth } : {} },
|
|
5916
7551
|
callbacks
|
|
5917
7552
|
);
|
|
5918
7553
|
}
|
|
@@ -5925,7 +7560,7 @@ async function daemonInfo(options, callbacks = {}) {
|
|
|
5925
7560
|
}
|
|
5926
7561
|
return controlCall(socket, { type: "info" }, callbacks);
|
|
5927
7562
|
}
|
|
5928
|
-
function controlCall(socket,
|
|
7563
|
+
function controlCall(socket, request2, callbacks) {
|
|
5929
7564
|
return new Promise((resolve3) => {
|
|
5930
7565
|
const assembler = new PartAssembler();
|
|
5931
7566
|
const parts = /* @__PURE__ */ new Map();
|
|
@@ -5984,7 +7619,7 @@ function controlCall(socket, request, callbacks) {
|
|
|
5984
7619
|
})
|
|
5985
7620
|
);
|
|
5986
7621
|
socket.on("error", () => void 0);
|
|
5987
|
-
socket.send(JSON.stringify(
|
|
7622
|
+
socket.send(JSON.stringify(request2));
|
|
5988
7623
|
});
|
|
5989
7624
|
}
|
|
5990
7625
|
function openControlSocket(port, token, timeoutMs = 3e3) {
|
|
@@ -6001,7 +7636,7 @@ function openControlSocket(port, token, timeoutMs = 3e3) {
|
|
|
6001
7636
|
}
|
|
6002
7637
|
function readDaemonFile(key) {
|
|
6003
7638
|
try {
|
|
6004
|
-
return JSON.parse((0,
|
|
7639
|
+
return JSON.parse((0, import_node_fs5.readFileSync)(daemonFile(key), "utf8"));
|
|
6005
7640
|
} catch {
|
|
6006
7641
|
return void 0;
|
|
6007
7642
|
}
|
|
@@ -6013,7 +7648,7 @@ async function connectDaemon({
|
|
|
6013
7648
|
}) {
|
|
6014
7649
|
const key = daemonKey(config);
|
|
6015
7650
|
const existing = readDaemonFile(key);
|
|
6016
|
-
if (existing && existing.version ===
|
|
7651
|
+
if (existing && existing.version === VERSION2) {
|
|
6017
7652
|
try {
|
|
6018
7653
|
return await openControlSocket(existing.port, existing.token);
|
|
6019
7654
|
} catch {
|
|
@@ -6023,11 +7658,11 @@ async function connectDaemon({
|
|
|
6023
7658
|
);
|
|
6024
7659
|
}
|
|
6025
7660
|
}
|
|
6026
|
-
const lock = (0,
|
|
6027
|
-
const owner = (0,
|
|
6028
|
-
if (
|
|
7661
|
+
const lock = (0, import_node_path5.join)(stateDir(), `daemon-${key}.lock`);
|
|
7662
|
+
const owner = (0, import_node_crypto4.randomUUID)();
|
|
7663
|
+
if (takeLock2(lock, owner)) {
|
|
6029
7664
|
try {
|
|
6030
|
-
const child = (0,
|
|
7665
|
+
const child = (0, import_node_child_process3.spawn)(
|
|
6031
7666
|
process.execPath,
|
|
6032
7667
|
[
|
|
6033
7668
|
entry,
|
|
@@ -6040,19 +7675,19 @@ async function connectDaemon({
|
|
|
6040
7675
|
log("info", `spawned daemon ${key} (pid ${child.pid})`);
|
|
6041
7676
|
return await waitForDaemon(key, existing?.pid, child.pid);
|
|
6042
7677
|
} finally {
|
|
6043
|
-
(0,
|
|
7678
|
+
(0, import_node_fs5.rmSync)(lock, { force: true });
|
|
6044
7679
|
}
|
|
6045
7680
|
}
|
|
6046
7681
|
return waitForDaemon(key, existing?.pid);
|
|
6047
7682
|
}
|
|
6048
|
-
function
|
|
7683
|
+
function takeLock2(lock, owner) {
|
|
6049
7684
|
try {
|
|
6050
|
-
const age = Date.now() - ((0,
|
|
7685
|
+
const age = Date.now() - ((0, import_node_fs5.statSync)(lock, { throwIfNoEntry: false })?.mtimeMs ?? 0);
|
|
6051
7686
|
if (age > 2e4)
|
|
6052
|
-
(0,
|
|
6053
|
-
const fd = (0,
|
|
6054
|
-
(0,
|
|
6055
|
-
(0,
|
|
7687
|
+
(0, import_node_fs5.rmSync)(lock, { force: true });
|
|
7688
|
+
const fd = (0, import_node_fs5.openSync)(lock, "wx");
|
|
7689
|
+
(0, import_node_fs5.writeFileSync)(fd, owner);
|
|
7690
|
+
(0, import_node_fs5.closeSync)(fd);
|
|
6056
7691
|
return true;
|
|
6057
7692
|
} catch {
|
|
6058
7693
|
return false;
|
|
@@ -6061,10 +7696,10 @@ function takeLock(lock, owner) {
|
|
|
6061
7696
|
async function waitForDaemon(key, stalePid, newPid) {
|
|
6062
7697
|
const deadline = Date.now() + 15e3;
|
|
6063
7698
|
while (Date.now() < deadline) {
|
|
6064
|
-
const
|
|
6065
|
-
if (
|
|
7699
|
+
const file2 = readDaemonFile(key);
|
|
7700
|
+
if (file2 && file2.pid !== stalePid && (newPid === void 0 || file2.pid === newPid) && file2.version === VERSION2) {
|
|
6066
7701
|
try {
|
|
6067
|
-
return await openControlSocket(
|
|
7702
|
+
return await openControlSocket(file2.port, file2.token);
|
|
6068
7703
|
} catch {
|
|
6069
7704
|
}
|
|
6070
7705
|
}
|
|
@@ -6075,6 +7710,370 @@ async function waitForDaemon(key, stalePid, newPid) {
|
|
|
6075
7710
|
);
|
|
6076
7711
|
}
|
|
6077
7712
|
|
|
7713
|
+
// src/login.ts
|
|
7714
|
+
var import_node_child_process4 = require("node:child_process");
|
|
7715
|
+
var import_node_crypto5 = require("node:crypto");
|
|
7716
|
+
var import_node_fs6 = require("node:fs");
|
|
7717
|
+
var import_node_http2 = require("node:http");
|
|
7718
|
+
var import_node_path6 = require("node:path");
|
|
7719
|
+
var LoginError = class extends Error {
|
|
7720
|
+
constructor(reason, message) {
|
|
7721
|
+
super(message);
|
|
7722
|
+
this.reason = reason;
|
|
7723
|
+
}
|
|
7724
|
+
};
|
|
7725
|
+
var LOOPBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
7726
|
+
function sitePage(origin, locale, path) {
|
|
7727
|
+
return `${origin}${locale === "en" ? "" : `/${locale}`}${path}`;
|
|
7728
|
+
}
|
|
7729
|
+
function pkcePair() {
|
|
7730
|
+
const verifier = (0, import_node_crypto5.randomBytes)(32).toString("base64url");
|
|
7731
|
+
const challenge = (0, import_node_crypto5.createHash)("sha256").update(verifier).digest("base64url");
|
|
7732
|
+
return { verifier, challenge };
|
|
7733
|
+
}
|
|
7734
|
+
function loginUrl(options) {
|
|
7735
|
+
const url = new URL(
|
|
7736
|
+
sitePage(options.origin, options.locale, TUTTICODE_OAUTH_PATHS.login)
|
|
7737
|
+
);
|
|
7738
|
+
url.search = new URLSearchParams({
|
|
7739
|
+
client_id: TUTTICODE_CLIENT_ID,
|
|
7740
|
+
redirect_uri: options.redirectUri,
|
|
7741
|
+
code_challenge: options.challenge,
|
|
7742
|
+
code_challenge_method: "S256",
|
|
7743
|
+
state: options.state,
|
|
7744
|
+
scope: TUTTICODE_SCOPE,
|
|
7745
|
+
resource: tutticodeResourceUrl(options.origin),
|
|
7746
|
+
device_name: options.deviceName
|
|
7747
|
+
}).toString();
|
|
7748
|
+
return url.toString();
|
|
7749
|
+
}
|
|
7750
|
+
var page = (title, text) => `<!doctype html><meta charset="utf-8"><title>${title}</title><p>${text}</p>`;
|
|
7751
|
+
async function startLoopback(options) {
|
|
7752
|
+
let resolveCode;
|
|
7753
|
+
let rejectCode;
|
|
7754
|
+
const code = new Promise((resolve3, reject) => {
|
|
7755
|
+
resolveCode = resolve3;
|
|
7756
|
+
rejectCode = reject;
|
|
7757
|
+
});
|
|
7758
|
+
code.catch(() => void 0);
|
|
7759
|
+
let settled = false;
|
|
7760
|
+
const finish = (fn) => {
|
|
7761
|
+
if (settled)
|
|
7762
|
+
return;
|
|
7763
|
+
settled = true;
|
|
7764
|
+
clearTimeout(timer);
|
|
7765
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
7766
|
+
fn();
|
|
7767
|
+
setImmediate(() => server.close());
|
|
7768
|
+
};
|
|
7769
|
+
const server = (0, import_node_http2.createServer)((req, res) => {
|
|
7770
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
7771
|
+
if (req.method !== "GET" || url.pathname !== "/callback") {
|
|
7772
|
+
res.writeHead(404).end();
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
const params = url.searchParams;
|
|
7776
|
+
if (settled || params.get("state") !== options.state) {
|
|
7777
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(
|
|
7778
|
+
page(
|
|
7779
|
+
"TuttiCode",
|
|
7780
|
+
"This sign-in link does not belong to the running tutticode login. Run tutticode login again."
|
|
7781
|
+
)
|
|
7782
|
+
);
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7785
|
+
const error = params.get("error");
|
|
7786
|
+
if (error) {
|
|
7787
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(page("TuttiCode", "Sign-in cancelled. You can close this tab."));
|
|
7788
|
+
finish(
|
|
7789
|
+
() => rejectCode(
|
|
7790
|
+
error === "access_denied" ? new LoginError(
|
|
7791
|
+
"denied",
|
|
7792
|
+
"The sign-in was cancelled in the browser."
|
|
7793
|
+
) : new LoginError("server", `The sign-in failed (${error}).`)
|
|
7794
|
+
)
|
|
7795
|
+
);
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
if (normalizeOrigin(params.get("iss") ?? "") !== options.issuer) {
|
|
7799
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(page("TuttiCode", "Sign-in refused: unexpected issuer."));
|
|
7800
|
+
finish(
|
|
7801
|
+
() => rejectCode(
|
|
7802
|
+
new LoginError(
|
|
7803
|
+
"issuer",
|
|
7804
|
+
`The sign-in came back from ${params.get("iss") ?? "an unnamed issuer"}, not ${options.issuer}.`
|
|
7805
|
+
)
|
|
7806
|
+
)
|
|
7807
|
+
);
|
|
7808
|
+
return;
|
|
7809
|
+
}
|
|
7810
|
+
const value = params.get("code");
|
|
7811
|
+
if (!value) {
|
|
7812
|
+
res.writeHead(400).end();
|
|
7813
|
+
return;
|
|
7814
|
+
}
|
|
7815
|
+
res.writeHead(302, { Location: options.doneUrl }).end();
|
|
7816
|
+
finish(() => resolveCode(value));
|
|
7817
|
+
});
|
|
7818
|
+
const onAbort = () => finish(() => rejectCode(new LoginError("cancelled", "Sign-in cancelled.")));
|
|
7819
|
+
const timer = setTimeout(
|
|
7820
|
+
() => finish(
|
|
7821
|
+
() => rejectCode(
|
|
7822
|
+
new LoginError(
|
|
7823
|
+
"timeout",
|
|
7824
|
+
`No answer from the browser within ${Math.round(
|
|
7825
|
+
(options.timeoutMs ?? LOOPBACK_TIMEOUT_MS) / 6e4
|
|
7826
|
+
)} minutes.`
|
|
7827
|
+
)
|
|
7828
|
+
)
|
|
7829
|
+
),
|
|
7830
|
+
options.timeoutMs ?? LOOPBACK_TIMEOUT_MS
|
|
7831
|
+
);
|
|
7832
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
7833
|
+
await new Promise((resolve3, reject) => {
|
|
7834
|
+
server.once("error", reject);
|
|
7835
|
+
server.listen(0, "127.0.0.1", resolve3);
|
|
7836
|
+
});
|
|
7837
|
+
const port = server.address().port;
|
|
7838
|
+
return {
|
|
7839
|
+
port,
|
|
7840
|
+
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
7841
|
+
code,
|
|
7842
|
+
close: () => finish(() => rejectCode(new LoginError("cancelled", "Closed.")))
|
|
7843
|
+
};
|
|
7844
|
+
}
|
|
7845
|
+
async function loopbackLogin(options) {
|
|
7846
|
+
const { verifier, challenge } = pkcePair();
|
|
7847
|
+
const state = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
|
|
7848
|
+
const loopback = await startLoopback({
|
|
7849
|
+
state,
|
|
7850
|
+
issuer: options.origin,
|
|
7851
|
+
doneUrl: sitePage(options.origin, options.locale, "/code/login/done"),
|
|
7852
|
+
timeoutMs: options.timeoutMs,
|
|
7853
|
+
signal: options.signal
|
|
7854
|
+
});
|
|
7855
|
+
const url = loginUrl({
|
|
7856
|
+
origin: options.origin,
|
|
7857
|
+
locale: options.locale,
|
|
7858
|
+
redirectUri: loopback.redirectUri,
|
|
7859
|
+
challenge,
|
|
7860
|
+
state,
|
|
7861
|
+
deviceName: options.deviceName
|
|
7862
|
+
});
|
|
7863
|
+
options.onUrl?.(url);
|
|
7864
|
+
try {
|
|
7865
|
+
await options.openBrowser(url);
|
|
7866
|
+
} catch {
|
|
7867
|
+
}
|
|
7868
|
+
const code = await loopback.code;
|
|
7869
|
+
return exchange(options.origin, options, {
|
|
7870
|
+
grant_type: "authorization_code",
|
|
7871
|
+
code,
|
|
7872
|
+
code_verifier: verifier,
|
|
7873
|
+
redirect_uri: loopback.redirectUri,
|
|
7874
|
+
resource: tutticodeResourceUrl(options.origin)
|
|
7875
|
+
});
|
|
7876
|
+
}
|
|
7877
|
+
async function exchange(origin, deps, params) {
|
|
7878
|
+
try {
|
|
7879
|
+
return tokenResponse(
|
|
7880
|
+
await postOAuth(origin, TUTTICODE_OAUTH_PATHS.token, params, deps)
|
|
7881
|
+
);
|
|
7882
|
+
} catch (e) {
|
|
7883
|
+
if (e instanceof OAuthError)
|
|
7884
|
+
throw new LoginError("server", `The sign-in failed: ${e.message}`);
|
|
7885
|
+
throw e;
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
async function startDeviceLogin(origin, deviceName2, deps = {}) {
|
|
7889
|
+
let body;
|
|
7890
|
+
try {
|
|
7891
|
+
body = await postOAuth(
|
|
7892
|
+
origin,
|
|
7893
|
+
TUTTICODE_OAUTH_PATHS.deviceAuthorization,
|
|
7894
|
+
{
|
|
7895
|
+
scope: TUTTICODE_SCOPE,
|
|
7896
|
+
resource: tutticodeResourceUrl(origin),
|
|
7897
|
+
device_name: deviceName2
|
|
7898
|
+
},
|
|
7899
|
+
deps
|
|
7900
|
+
);
|
|
7901
|
+
} catch (e) {
|
|
7902
|
+
if (e instanceof OAuthError)
|
|
7903
|
+
throw new LoginError(
|
|
7904
|
+
"server",
|
|
7905
|
+
e.error === "slow_down" ? "Too many sign-in attempts from this network: try again in an hour." : `The sign-in could not start: ${e.message}`
|
|
7906
|
+
);
|
|
7907
|
+
throw e;
|
|
7908
|
+
}
|
|
7909
|
+
if (typeof body.device_code !== "string" || typeof body.user_code !== "string" || typeof body.verification_uri !== "string")
|
|
7910
|
+
throw new LoginError("server", "The server sent no device code.");
|
|
7911
|
+
return {
|
|
7912
|
+
deviceCode: body.device_code,
|
|
7913
|
+
userCode: body.user_code,
|
|
7914
|
+
verificationUri: body.verification_uri,
|
|
7915
|
+
...typeof body.verification_uri_complete === "string" ? { verificationUriComplete: body.verification_uri_complete } : {},
|
|
7916
|
+
expiresIn: positive(body.expires_in, 600),
|
|
7917
|
+
interval: positive(body.interval, 5)
|
|
7918
|
+
};
|
|
7919
|
+
}
|
|
7920
|
+
var positive = (v, fallback) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
|
|
7921
|
+
var SLOW_DOWN_STEP_S = 5;
|
|
7922
|
+
async function pollDeviceToken(origin, device, options = {}) {
|
|
7923
|
+
const now = options.now ?? Date.now;
|
|
7924
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r2) => setTimeout(r2, ms)));
|
|
7925
|
+
const deadline = now() + device.expiresIn * 1e3;
|
|
7926
|
+
let interval = device.interval;
|
|
7927
|
+
for (; ; ) {
|
|
7928
|
+
await sleep(interval * 1e3);
|
|
7929
|
+
if (options.signal?.aborted)
|
|
7930
|
+
throw new LoginError("cancelled", "Sign-in cancelled.");
|
|
7931
|
+
if (options.stillWanted && !options.stillWanted())
|
|
7932
|
+
throw new LoginError("cancelled", "A newer sign-in replaced this one.");
|
|
7933
|
+
if (now() >= deadline)
|
|
7934
|
+
throw new LoginError(
|
|
7935
|
+
"expired",
|
|
7936
|
+
"The code expired before it was approved: run tutticode login --device again."
|
|
7937
|
+
);
|
|
7938
|
+
try {
|
|
7939
|
+
return tokenResponse(
|
|
7940
|
+
await postOAuth(
|
|
7941
|
+
origin,
|
|
7942
|
+
TUTTICODE_OAUTH_PATHS.token,
|
|
7943
|
+
{
|
|
7944
|
+
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
7945
|
+
device_code: device.deviceCode
|
|
7946
|
+
},
|
|
7947
|
+
options
|
|
7948
|
+
)
|
|
7949
|
+
);
|
|
7950
|
+
} catch (e) {
|
|
7951
|
+
if (!(e instanceof OAuthError))
|
|
7952
|
+
continue;
|
|
7953
|
+
switch (e.error) {
|
|
7954
|
+
case "authorization_pending":
|
|
7955
|
+
continue;
|
|
7956
|
+
case "slow_down": {
|
|
7957
|
+
const named = Number(e.body.interval);
|
|
7958
|
+
interval = Number.isFinite(named) && named > interval ? named : interval + SLOW_DOWN_STEP_S;
|
|
7959
|
+
continue;
|
|
7960
|
+
}
|
|
7961
|
+
case "access_denied":
|
|
7962
|
+
throw new LoginError("denied", "The sign-in was declined.");
|
|
7963
|
+
case "expired_token":
|
|
7964
|
+
throw new LoginError(
|
|
7965
|
+
"expired",
|
|
7966
|
+
"The code expired before it was approved: run tutticode login --device again."
|
|
7967
|
+
);
|
|
7968
|
+
default:
|
|
7969
|
+
throw new LoginError("server", `The sign-in failed: ${e.message}`);
|
|
7970
|
+
}
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7974
|
+
async function completeLogin(origin, tokens, deviceName2, deps = {}) {
|
|
7975
|
+
const now = (deps.now ?? Date.now)();
|
|
7976
|
+
const previous = readAccount(origin);
|
|
7977
|
+
try {
|
|
7978
|
+
await saveAccount(origin, storedAccount(tokens, now, { deviceName: deviceName2 }));
|
|
7979
|
+
} catch (e) {
|
|
7980
|
+
await revokeToken(origin, tokens.refresh_token, {
|
|
7981
|
+
...deps,
|
|
7982
|
+
timeoutMs: 5e3
|
|
7983
|
+
});
|
|
7984
|
+
throw e;
|
|
7985
|
+
}
|
|
7986
|
+
const state = await accountState(origin, deps, { envToken: false });
|
|
7987
|
+
if (state.state === "signed-in") {
|
|
7988
|
+
const me = state.me;
|
|
7989
|
+
const current = readAccount(origin);
|
|
7990
|
+
if (current?.accessToken === tokens.access_token)
|
|
7991
|
+
await saveAccount(origin, {
|
|
7992
|
+
...current,
|
|
7993
|
+
userId: me.user.id,
|
|
7994
|
+
email: me.user.email,
|
|
7995
|
+
nickname: me.user.nickname,
|
|
7996
|
+
plan: me.plan,
|
|
7997
|
+
...me.expiresAt ? { grantExpiresAt: me.expiresAt } : {},
|
|
7998
|
+
...me.deviceName ? { deviceName: me.deviceName } : {}
|
|
7999
|
+
});
|
|
8000
|
+
}
|
|
8001
|
+
if (previous && previous.refreshToken !== tokens.refresh_token)
|
|
8002
|
+
await revokeToken(origin, previous.refreshToken, {
|
|
8003
|
+
...deps,
|
|
8004
|
+
timeoutMs: 5e3
|
|
8005
|
+
});
|
|
8006
|
+
return state;
|
|
8007
|
+
}
|
|
8008
|
+
function pendingLoginPath(origin) {
|
|
8009
|
+
const key = (0, import_node_crypto5.createHash)("sha256").update(origin).digest("hex").slice(0, 12);
|
|
8010
|
+
return (0, import_node_path6.join)(stateDir(), `login-pending-${key}.json`);
|
|
8011
|
+
}
|
|
8012
|
+
function writePendingLogin(pending) {
|
|
8013
|
+
const path = pendingLoginPath(pending.origin);
|
|
8014
|
+
writePrivateFile(path, JSON.stringify(pending));
|
|
8015
|
+
return path;
|
|
8016
|
+
}
|
|
8017
|
+
function readPendingLogin(path) {
|
|
8018
|
+
try {
|
|
8019
|
+
const value = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
|
|
8020
|
+
return typeof value.deviceCode === "string" && typeof value.origin === "string" ? value : void 0;
|
|
8021
|
+
} catch {
|
|
8022
|
+
return void 0;
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
function pendingLogin(origin, now = Date.now()) {
|
|
8026
|
+
const pending = readPendingLogin(pendingLoginPath(origin));
|
|
8027
|
+
return pending && Date.parse(pending.expiresAt) > now ? pending : void 0;
|
|
8028
|
+
}
|
|
8029
|
+
function spawnLoginPoller(entry, file2) {
|
|
8030
|
+
const child = (0, import_node_child_process4.spawn)(process.execPath, [entry, "__login-poll", file2], {
|
|
8031
|
+
detached: true,
|
|
8032
|
+
stdio: "ignore",
|
|
8033
|
+
windowsHide: true
|
|
8034
|
+
});
|
|
8035
|
+
child.unref();
|
|
8036
|
+
}
|
|
8037
|
+
async function runLoginPoller(file2, log, deps = {}) {
|
|
8038
|
+
const pending = readPendingLogin(file2);
|
|
8039
|
+
if (!pending) {
|
|
8040
|
+
log("warn", "login poller: no pending sign-in");
|
|
8041
|
+
return false;
|
|
8042
|
+
}
|
|
8043
|
+
const remaining = Math.max(
|
|
8044
|
+
1,
|
|
8045
|
+
Math.round((Date.parse(pending.expiresAt) - Date.now()) / 1e3)
|
|
8046
|
+
);
|
|
8047
|
+
try {
|
|
8048
|
+
const tokens = await pollDeviceToken(
|
|
8049
|
+
pending.origin,
|
|
8050
|
+
{
|
|
8051
|
+
deviceCode: pending.deviceCode,
|
|
8052
|
+
expiresIn: remaining,
|
|
8053
|
+
interval: pending.interval
|
|
8054
|
+
},
|
|
8055
|
+
{
|
|
8056
|
+
...deps,
|
|
8057
|
+
stillWanted: () => readPendingLogin(file2)?.deviceCode === pending.deviceCode
|
|
8058
|
+
}
|
|
8059
|
+
);
|
|
8060
|
+
const state = await completeLogin(
|
|
8061
|
+
pending.origin,
|
|
8062
|
+
tokens,
|
|
8063
|
+
pending.deviceName,
|
|
8064
|
+
deps
|
|
8065
|
+
);
|
|
8066
|
+
log("info", `login poller: signed in (${state.state})`);
|
|
8067
|
+
return true;
|
|
8068
|
+
} catch (e) {
|
|
8069
|
+
log("info", `login poller: ${e.message}`);
|
|
8070
|
+
return false;
|
|
8071
|
+
} finally {
|
|
8072
|
+
if (readPendingLogin(file2)?.deviceCode === pending.deviceCode)
|
|
8073
|
+
(0, import_node_fs6.rmSync)(file2, { force: true });
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
|
|
6078
8077
|
// src/mcp.ts
|
|
6079
8078
|
var import_node_readline = require("node:readline");
|
|
6080
8079
|
|
|
@@ -6084,13 +8083,13 @@ var JSON_RPC_INVALID_REQUEST = -32600;
|
|
|
6084
8083
|
var JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
6085
8084
|
var JSON_RPC_INVALID_PARAMS = -32602;
|
|
6086
8085
|
var JSON_RPC_INTERNAL_ERROR = -32603;
|
|
6087
|
-
var
|
|
8086
|
+
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6088
8087
|
var isId2 = (v) => typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
6089
8088
|
function classifyMessage(raw) {
|
|
6090
|
-
if (!
|
|
8089
|
+
if (!isObject3(raw) || raw.jsonrpc !== "2.0") {
|
|
6091
8090
|
return {
|
|
6092
8091
|
kind: "invalid",
|
|
6093
|
-
id:
|
|
8092
|
+
id: isObject3(raw) && isId2(raw.id) ? raw.id : null,
|
|
6094
8093
|
reason: "Not a JSON-RPC 2.0 message"
|
|
6095
8094
|
};
|
|
6096
8095
|
}
|
|
@@ -6104,7 +8103,7 @@ function classifyMessage(raw) {
|
|
|
6104
8103
|
reason: "Missing method"
|
|
6105
8104
|
};
|
|
6106
8105
|
}
|
|
6107
|
-
if (raw.params !== void 0 && !
|
|
8106
|
+
if (raw.params !== void 0 && !isObject3(raw.params)) {
|
|
6108
8107
|
return {
|
|
6109
8108
|
kind: "invalid",
|
|
6110
8109
|
id: isId2(raw.id) ? raw.id : null,
|
|
@@ -6287,22 +8286,22 @@ function createMcpServer(options) {
|
|
|
6287
8286
|
}
|
|
6288
8287
|
|
|
6289
8288
|
// src/reference.ts
|
|
6290
|
-
var
|
|
6291
|
-
var
|
|
8289
|
+
var import_node_fs7 = require("node:fs");
|
|
8290
|
+
var import_node_path7 = require("node:path");
|
|
6292
8291
|
var URI_PREFIX = "tutticode://skills/";
|
|
6293
8292
|
var cache;
|
|
6294
8293
|
function loadReference(dir = skillsDir()) {
|
|
6295
8294
|
if (cache?.dir === dir)
|
|
6296
8295
|
return cache.docs;
|
|
6297
8296
|
const docs = [];
|
|
6298
|
-
if ((0,
|
|
6299
|
-
for (const skill of (0,
|
|
6300
|
-
const root = (0,
|
|
6301
|
-
if (!(0,
|
|
8297
|
+
if ((0, import_node_fs7.existsSync)(dir))
|
|
8298
|
+
for (const skill of (0, import_node_fs7.readdirSync)(dir).sort()) {
|
|
8299
|
+
const root = (0, import_node_path7.join)(dir, skill);
|
|
8300
|
+
if (!(0, import_node_fs7.statSync)(root).isDirectory() || !(0, import_node_fs7.existsSync)((0, import_node_path7.join)(root, "SKILL.md")))
|
|
6302
8301
|
continue;
|
|
6303
|
-
for (const
|
|
6304
|
-
const path = (0,
|
|
6305
|
-
const text = (0,
|
|
8302
|
+
for (const file2 of walk(root)) {
|
|
8303
|
+
const path = (0, import_node_path7.relative)(root, file2).split(import_node_path7.sep).join("/");
|
|
8304
|
+
const text = (0, import_node_fs7.readFileSync)(file2, "utf8").replace(/\r\n/g, "\n");
|
|
6306
8305
|
docs.push({
|
|
6307
8306
|
uri: `${URI_PREFIX}${skill}/${path}`,
|
|
6308
8307
|
skill,
|
|
@@ -6316,9 +8315,9 @@ function loadReference(dir = skillsDir()) {
|
|
|
6316
8315
|
return docs;
|
|
6317
8316
|
}
|
|
6318
8317
|
function walk(dir) {
|
|
6319
|
-
return (0,
|
|
6320
|
-
const path = (0,
|
|
6321
|
-
if ((0,
|
|
8318
|
+
return (0, import_node_fs7.readdirSync)(dir).sort().flatMap((name) => {
|
|
8319
|
+
const path = (0, import_node_path7.join)(dir, name);
|
|
8320
|
+
if ((0, import_node_fs7.statSync)(path).isDirectory())
|
|
6322
8321
|
return walk(path);
|
|
6323
8322
|
return name.endsWith(".md") ? [path] : [];
|
|
6324
8323
|
});
|
|
@@ -6398,8 +8397,8 @@ function count(haystack, needle) {
|
|
|
6398
8397
|
}
|
|
6399
8398
|
|
|
6400
8399
|
// src/tools.ts
|
|
6401
|
-
var
|
|
6402
|
-
var
|
|
8400
|
+
var import_node_os4 = require("node:os");
|
|
8401
|
+
var import_node_path8 = require("node:path");
|
|
6403
8402
|
|
|
6404
8403
|
// ../mcp-protocol/src/code-tools.ts
|
|
6405
8404
|
var CODE_CHECK_TOOL = "code_check";
|
|
@@ -6956,7 +8955,9 @@ function codeFragment(code) {
|
|
|
6956
8955
|
// src/tools.ts
|
|
6957
8956
|
var CODE_OPEN_TOOL = "code_open";
|
|
6958
8957
|
var CODE_REFERENCE_TOOL = "code_reference";
|
|
6959
|
-
var
|
|
8958
|
+
var CODE_ACCOUNT_TOOL = "code_account";
|
|
8959
|
+
var BACKEND_NOTE = "Runs locally: tutticode renders in your own browser with the tuttidaw.com/code engine (free).";
|
|
8960
|
+
var LIMITS_NOTE = `Once TuttiDAW switches its limits on (being rolled out; until then nothing is cut or tagged), renders without sign-in are limited to ${DEFAULT_TUTTICODE_LIMITS.anonMaxSeconds} s (error code "limit") and renders over ${DEFAULT_TUTTICODE_LIMITS.anonWatermarkAfterSeconds} s carry a spoken "register at TuttiDAW" tag, reported as \`watermark\`: it is not part of the program and is in the stats; never change the code to remove it, tell the user once that \`tutticode login\` (free) removes it.`;
|
|
6960
8961
|
var codeProperty2 = codeCheckTool.inputSchema.properties.code;
|
|
6961
8962
|
var renderProperties2 = {
|
|
6962
8963
|
code: codeProperty2,
|
|
@@ -7015,13 +9016,17 @@ var renderProperties2 = {
|
|
|
7015
9016
|
type: "number",
|
|
7016
9017
|
minimum: -40,
|
|
7017
9018
|
maximum: -5,
|
|
7018
|
-
description: 'Static gain to this integrated loudness (e.g. -14 for streaming, -16 for podcasts / video beds). Fails with code "clip"
|
|
9019
|
+
description: 'Static gain to this integrated loudness (e.g. -14 for streaming, -16 for podcasts / video beds). Fails with code "clip" when truePeak cannot be met, unless limit is true.'
|
|
7019
9020
|
},
|
|
7020
9021
|
truePeak: {
|
|
7021
9022
|
type: "number",
|
|
7022
9023
|
minimum: -12,
|
|
7023
9024
|
maximum: 0,
|
|
7024
|
-
description: "dBTP ceiling
|
|
9025
|
+
description: "dBTP ceiling (default -1). Without lufs, a render that would pass it is lowered by one static gain (loudness.gainDb, warnings): no file is written clipped."
|
|
9026
|
+
},
|
|
9027
|
+
limit: {
|
|
9028
|
+
type: "boolean",
|
|
9029
|
+
description: 'Opt-in true-peak limiter: peaks over truePeak are limited instead of failing with "clip" (with lufs) or lowering the whole render (without). Use it to reach -14 / -16 LUFS on drum-heavy programs; loudness.limitDb reports the most it took off. Stems are not limited.'
|
|
7025
9030
|
},
|
|
7026
9031
|
out: {
|
|
7027
9032
|
type: "string",
|
|
@@ -7030,6 +9035,10 @@ var renderProperties2 = {
|
|
|
7030
9035
|
stemsDir: {
|
|
7031
9036
|
type: "string",
|
|
7032
9037
|
description: "Folder for the stems (default: next to out, <name>.stems)."
|
|
9038
|
+
},
|
|
9039
|
+
brandTag: {
|
|
9040
|
+
type: "boolean",
|
|
9041
|
+
description: 'false: no "Made with TuttiCode" metadata tag in the files. Honoured only when signed in (default true).'
|
|
7033
9042
|
}
|
|
7034
9043
|
};
|
|
7035
9044
|
var localCheckTool = {
|
|
@@ -7039,7 +9048,7 @@ var localCheckTool = {
|
|
|
7039
9048
|
var localRenderTool = {
|
|
7040
9049
|
name: "code_render",
|
|
7041
9050
|
title: "Render TuttiCode to a file",
|
|
7042
|
-
description: `Render a TuttiCode program to an audio file on this computer (the same instruments and effects as tuttidaw.com/code) and measure it: integrated LUFS, true peak, octave bands, stereo width, groove, key, silence, per-bar loudness and each voice's level. Returns the written file paths (audio is never inlined), the beat grid (seconds of each bar and beat, for syncing cuts) and an editUrl - give it to the user so they can open the program in TuttiDAW and keep editing. Lengths up to ${BRIDGE_MAX_SECONDS} s. Call code_check first. ${BACKEND_NOTE}`,
|
|
9051
|
+
description: `Render a TuttiCode program to an audio file on this computer (the same instruments and effects as tuttidaw.com/code) and measure it: integrated LUFS, true peak, octave bands, stereo width, groove, key, silence, per-bar loudness and each voice's level. Returns the written file paths (audio is never inlined), the beat grid (seconds of each bar and beat, for syncing cuts) and an editUrl - give it to the user so they can open the program in TuttiDAW and keep editing. Lengths up to ${BRIDGE_MAX_SECONDS} s. Call code_check first. ${LIMITS_NOTE} ${BACKEND_NOTE}`,
|
|
7043
9052
|
inputSchema: {
|
|
7044
9053
|
type: "object",
|
|
7045
9054
|
properties: renderProperties2,
|
|
@@ -7118,14 +9127,30 @@ var codeReferenceTool = {
|
|
|
7118
9127
|
openWorldHint: false
|
|
7119
9128
|
}
|
|
7120
9129
|
};
|
|
9130
|
+
var codeAccountTool = {
|
|
9131
|
+
name: CODE_ACCOUNT_TOOL,
|
|
9132
|
+
title: "TuttiCode sign-in and limits",
|
|
9133
|
+
description: "Who renders and with which limits: signedIn, plan, limits.maxSeconds (longest render) and limits.watermarkAfterSeconds (renders longer than this carry a spoken tag; null = never), usageToday, and when not signed in a loginHint. Use it to plan within the limits (keep an anonymous draft at or under watermarkAfterSeconds). Never starts a sign-in: to sign in, run the loginHint command and ask the user to approve the code it prints.",
|
|
9134
|
+
inputSchema: {
|
|
9135
|
+
type: "object",
|
|
9136
|
+
properties: {},
|
|
9137
|
+
additionalProperties: false
|
|
9138
|
+
},
|
|
9139
|
+
annotations: {
|
|
9140
|
+
readOnlyHint: true,
|
|
9141
|
+
idempotentHint: true,
|
|
9142
|
+
openWorldHint: true
|
|
9143
|
+
}
|
|
9144
|
+
};
|
|
7121
9145
|
var LOCAL_TOOLS = [
|
|
7122
9146
|
localCheckTool,
|
|
7123
9147
|
localRenderTool,
|
|
7124
9148
|
localCompareTool,
|
|
7125
9149
|
codeOpenTool,
|
|
7126
|
-
codeReferenceTool
|
|
9150
|
+
codeReferenceTool,
|
|
9151
|
+
codeAccountTool
|
|
7127
9152
|
];
|
|
7128
|
-
var MCP_INSTRUCTIONS = `tutticode turns TuttiCode (a superset of Strudel) into audio on this computer, rendering in the user's browser with the tuttidaw.com/code engine. Loop: write code -> code_check (fix every error) -> code_render (files + statistics) -> read the stats / code_compare -> revise. Use code_reference when unsure of a function. Always give the user the editUrl from code_render so they can keep editing in TuttiDAW.`;
|
|
9153
|
+
var MCP_INSTRUCTIONS = `tutticode turns TuttiCode (a superset of Strudel) into audio on this computer, rendering in the user's browser with the tuttidaw.com/code engine. Loop: write code -> code_check (fix every error) -> code_render (files + statistics) -> read the stats / code_compare -> revise. Use code_reference when unsure of a function. Always give the user the editUrl from code_render so they can keep editing in TuttiDAW. code_account says whether renders are signed in and their limits. On a "limit" error, tell the user its message in one sentence and carry on within the limit; a \`watermark\` in a report is the anonymous voice tag, not part of the program: do not change the code to remove it.`;
|
|
7129
9154
|
var MAX_STORED = 64;
|
|
7130
9155
|
var CHECK_TIMEOUT_MS = 9e4;
|
|
7131
9156
|
var RENDER_TIMEOUT_MS = 15 * 6e4;
|
|
@@ -7144,7 +9169,7 @@ function createToolHandler(deps) {
|
|
|
7144
9169
|
async function check2(args, signal) {
|
|
7145
9170
|
const command = {
|
|
7146
9171
|
type: "check",
|
|
7147
|
-
id: (0,
|
|
9172
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
7148
9173
|
code: args.code,
|
|
7149
9174
|
...args.bpm !== void 0 ? { bpm: args.bpm } : {}
|
|
7150
9175
|
};
|
|
@@ -7176,7 +9201,7 @@ function createToolHandler(deps) {
|
|
|
7176
9201
|
`format ${String(args.format)} does not match out ${out}.`
|
|
7177
9202
|
);
|
|
7178
9203
|
const format = args.format ?? outFormat ?? "wav";
|
|
7179
|
-
const id = (0,
|
|
9204
|
+
const id = (0, import_node_crypto3.randomUUID)();
|
|
7180
9205
|
const length = lengths.length ? { [lengths[0]]: args[lengths[0]] } : void 0;
|
|
7181
9206
|
const command = {
|
|
7182
9207
|
type: "render",
|
|
@@ -7195,7 +9220,9 @@ function createToolHandler(deps) {
|
|
|
7195
9220
|
"fadeOut",
|
|
7196
9221
|
"tail",
|
|
7197
9222
|
"lufs",
|
|
7198
|
-
"truePeak"
|
|
9223
|
+
"truePeak",
|
|
9224
|
+
"limit",
|
|
9225
|
+
"brandTag"
|
|
7199
9226
|
])
|
|
7200
9227
|
if (args[key] !== void 0)
|
|
7201
9228
|
command[key] = args[key];
|
|
@@ -7209,8 +9236,8 @@ function createToolHandler(deps) {
|
|
|
7209
9236
|
});
|
|
7210
9237
|
if (outcome.kind !== "rendered")
|
|
7211
9238
|
return errorResult(outcome);
|
|
7212
|
-
const path = out ?? (0,
|
|
7213
|
-
deps.outDir ?? (0,
|
|
9239
|
+
const path = out ?? (0, import_node_path8.join)(
|
|
9240
|
+
deps.outDir ?? (0, import_node_path8.join)((0, import_node_os4.tmpdir)(), "tutticode"),
|
|
7214
9241
|
`render-${id.slice(0, 8)}.${format}`
|
|
7215
9242
|
);
|
|
7216
9243
|
const files = writeRender(
|
|
@@ -7219,10 +9246,15 @@ function createToolHandler(deps) {
|
|
|
7219
9246
|
path,
|
|
7220
9247
|
typeof args.stemsDir === "string" ? args.stemsDir : void 0
|
|
7221
9248
|
);
|
|
7222
|
-
remember(id, {
|
|
9249
|
+
remember(id, {
|
|
9250
|
+
meta: outcome.meta,
|
|
9251
|
+
analysis: outcome.meta.analysis,
|
|
9252
|
+
watermark: outcome.meta.watermark
|
|
9253
|
+
});
|
|
7223
9254
|
const { meta } = outcome;
|
|
7224
9255
|
return {
|
|
7225
9256
|
id,
|
|
9257
|
+
watermarked: !!meta.watermark,
|
|
7226
9258
|
report: {
|
|
7227
9259
|
id,
|
|
7228
9260
|
kind: "render",
|
|
@@ -7241,14 +9273,18 @@ function createToolHandler(deps) {
|
|
|
7241
9273
|
loudness: {
|
|
7242
9274
|
lufs: meta.lufs,
|
|
7243
9275
|
truePeak: meta.truePeak,
|
|
7244
|
-
gainDb: meta.gainDb
|
|
9276
|
+
gainDb: meta.gainDb,
|
|
9277
|
+
...meta.limitDb ? { limitDb: meta.limitDb } : {}
|
|
7245
9278
|
},
|
|
9279
|
+
...meta.warnings?.length ? { warnings: meta.warnings } : {},
|
|
7246
9280
|
voices: meta.voices,
|
|
7247
9281
|
barTimes: meta.barTimes,
|
|
7248
9282
|
beatTimes: meta.beatTimes,
|
|
7249
9283
|
check: meta.check,
|
|
7250
9284
|
...meta.analysis ? { analysis: meta.analysis } : {},
|
|
7251
|
-
engineVersion: meta.engineVersion
|
|
9285
|
+
engineVersion: meta.engineVersion,
|
|
9286
|
+
...meta.watermark ? { watermark: meta.watermark, watermarkNote: WATERMARK_LINE } : {},
|
|
9287
|
+
...signInNote(outcome.auth)
|
|
7252
9288
|
}
|
|
7253
9289
|
};
|
|
7254
9290
|
}
|
|
@@ -7288,9 +9324,11 @@ function createToolHandler(deps) {
|
|
|
7288
9324
|
return invalid("Give goals or againstRenderId.");
|
|
7289
9325
|
let analysis;
|
|
7290
9326
|
let rendered;
|
|
9327
|
+
let tagged = target === "render" && !!renders.get(String(args.againstRenderId))?.watermark;
|
|
7291
9328
|
if (args.renderId !== void 0) {
|
|
7292
9329
|
if (args.code !== void 0)
|
|
7293
9330
|
return invalid("Give renderId or code, not both.");
|
|
9331
|
+
tagged ||= !!renders.get(String(args.renderId))?.watermark;
|
|
7294
9332
|
analysis = renders.get(String(args.renderId))?.analysis;
|
|
7295
9333
|
if (!analysis)
|
|
7296
9334
|
return invalid(
|
|
@@ -7308,16 +9346,34 @@ function createToolHandler(deps) {
|
|
|
7308
9346
|
rendered = await render2(renderArgs, signal);
|
|
7309
9347
|
if (!("report" in rendered))
|
|
7310
9348
|
return rendered;
|
|
9349
|
+
tagged ||= rendered.watermarked;
|
|
7311
9350
|
analysis = rendered.report.analysis;
|
|
7312
9351
|
if (!analysis)
|
|
7313
9352
|
return invalid("The render came back without an analysis.");
|
|
7314
9353
|
} else
|
|
7315
9354
|
return invalid("Give renderId or code.");
|
|
7316
9355
|
const result = compareAnalysis(analysis, goals, target);
|
|
9356
|
+
const note = tagged ? { watermarkNote: WATERMARK_LINE } : {};
|
|
7317
9357
|
return textResult(
|
|
7318
|
-
rendered && "report" in rendered ? { ...rendered.report, compare: result } : { id: args.renderId, compare: result }
|
|
9358
|
+
rendered && "report" in rendered ? { ...rendered.report, compare: result, ...note } : { id: args.renderId, compare: result, ...note }
|
|
7319
9359
|
);
|
|
7320
9360
|
}
|
|
9361
|
+
async function account() {
|
|
9362
|
+
const state = deps.account ? await deps.account() : { state: "anonymous" };
|
|
9363
|
+
const signedIn = state.state === "signed-in";
|
|
9364
|
+
return textResult({
|
|
9365
|
+
signedIn,
|
|
9366
|
+
tokenKind: signedIn ? state.tokenKind : "anonymous",
|
|
9367
|
+
...signedIn ? {
|
|
9368
|
+
plan: state.me.plan,
|
|
9369
|
+
usageToday: state.me.usageToday,
|
|
9370
|
+
expiresAt: state.me.expiresAt
|
|
9371
|
+
} : {},
|
|
9372
|
+
limits: accountLimits(state),
|
|
9373
|
+
...signedIn ? {} : { loginHint: LOGIN_HINT },
|
|
9374
|
+
...state.state === "rejected" || state.state === "unreachable" ? { problem: state.message } : {}
|
|
9375
|
+
});
|
|
9376
|
+
}
|
|
7321
9377
|
return async function callTool(name, args, signal) {
|
|
7322
9378
|
switch (name) {
|
|
7323
9379
|
case "code_check":
|
|
@@ -7352,6 +9408,8 @@ function createToolHandler(deps) {
|
|
|
7352
9408
|
}
|
|
7353
9409
|
case CODE_REFERENCE_TOOL:
|
|
7354
9410
|
return reference(args);
|
|
9411
|
+
case CODE_ACCOUNT_TOOL:
|
|
9412
|
+
return account();
|
|
7355
9413
|
default:
|
|
7356
9414
|
return invalid(`Unknown tool ${name}.`);
|
|
7357
9415
|
}
|
|
@@ -7385,12 +9443,26 @@ function errorResult(outcome) {
|
|
|
7385
9443
|
return invalid(`Unexpected ${outcome.kind}.`);
|
|
7386
9444
|
return textResult(
|
|
7387
9445
|
{
|
|
7388
|
-
error: {
|
|
7389
|
-
|
|
9446
|
+
error: {
|
|
9447
|
+
code: outcome.code,
|
|
9448
|
+
message: outcome.message,
|
|
9449
|
+
...outcome.limit ? { limit: outcome.limit } : {}
|
|
9450
|
+
},
|
|
9451
|
+
...outcome.code === "limit" ? { loginHint: LOGIN_HINT } : {},
|
|
9452
|
+
...outcome.report ? { report: outcome.report } : {},
|
|
9453
|
+
...signInNote(outcome.auth)
|
|
7390
9454
|
},
|
|
7391
9455
|
true
|
|
7392
9456
|
);
|
|
7393
9457
|
}
|
|
9458
|
+
function signInNote(auth) {
|
|
9459
|
+
return auth?.rejected ? {
|
|
9460
|
+
signIn: {
|
|
9461
|
+
rejected: auth.rejected,
|
|
9462
|
+
note: "This ran without sign-in. Tell the user to run `tutticode login` again."
|
|
9463
|
+
}
|
|
9464
|
+
} : {};
|
|
9465
|
+
}
|
|
7394
9466
|
var GOAL_KEYS = {
|
|
7395
9467
|
style: (v) => typeof v === "string" && v.length > 0 && v.length <= 64,
|
|
7396
9468
|
lufs: (v) => num(v, -70, 0),
|
|
@@ -7432,10 +9504,11 @@ function serveMcpStdio(options) {
|
|
|
7432
9504
|
backend: options.backend,
|
|
7433
9505
|
styles: options.styles,
|
|
7434
9506
|
codeLink: options.codeLink,
|
|
7435
|
-
canShow: options.canShow
|
|
9507
|
+
canShow: options.canShow,
|
|
9508
|
+
account: options.account
|
|
7436
9509
|
});
|
|
7437
9510
|
const server = createMcpServer({
|
|
7438
|
-
serverInfo: { name: "tutticode", title: "TuttiCode", version:
|
|
9511
|
+
serverInfo: { name: "tutticode", title: "TuttiCode", version: VERSION2 },
|
|
7439
9512
|
instructions: MCP_INSTRUCTIONS,
|
|
7440
9513
|
listTools: () => [...LOCAL_TOOLS],
|
|
7441
9514
|
callTool: (name, args, ctx) => callTool(name, args, ctx.signal),
|
|
@@ -7473,7 +9546,7 @@ function serveMcpStdio(options) {
|
|
|
7473
9546
|
});
|
|
7474
9547
|
});
|
|
7475
9548
|
}
|
|
7476
|
-
function sessionBackend(create) {
|
|
9549
|
+
function sessionBackend(create, auth) {
|
|
7477
9550
|
const sessions = /* @__PURE__ */ new Map();
|
|
7478
9551
|
const get = (mode) => {
|
|
7479
9552
|
let session = sessions.get(mode);
|
|
@@ -7484,21 +9557,57 @@ function sessionBackend(create) {
|
|
|
7484
9557
|
return session;
|
|
7485
9558
|
};
|
|
7486
9559
|
return {
|
|
7487
|
-
backend: (command, { mode, signal, timeoutMs }) =>
|
|
9560
|
+
backend: async (command, { mode, signal, timeoutMs }) => {
|
|
9561
|
+
const signIn = await auth?.();
|
|
9562
|
+
const outcome = await get(mode).run(command, {
|
|
9563
|
+
signal,
|
|
9564
|
+
timeoutMs,
|
|
9565
|
+
...signIn ? { auth: signIn.auth } : {}
|
|
9566
|
+
});
|
|
9567
|
+
return signIn?.ended ? {
|
|
9568
|
+
...outcome,
|
|
9569
|
+
auth: { signedIn: false, ...outcome.auth, rejected: signIn.ended }
|
|
9570
|
+
} : outcome;
|
|
9571
|
+
},
|
|
7488
9572
|
close: async () => {
|
|
7489
9573
|
await Promise.all([...sessions.values()].map((s) => s.close()));
|
|
7490
9574
|
}
|
|
7491
9575
|
};
|
|
7492
9576
|
}
|
|
7493
9577
|
|
|
9578
|
+
// src/notices.ts
|
|
9579
|
+
var import_node_fs8 = require("node:fs");
|
|
9580
|
+
var import_node_path9 = require("node:path");
|
|
9581
|
+
var file = () => (0, import_node_path9.join)(stateDir(), "notices.json");
|
|
9582
|
+
function read() {
|
|
9583
|
+
try {
|
|
9584
|
+
const value = JSON.parse((0, import_node_fs8.readFileSync)(file(), "utf8"));
|
|
9585
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
9586
|
+
} catch {
|
|
9587
|
+
return {};
|
|
9588
|
+
}
|
|
9589
|
+
}
|
|
9590
|
+
function firstTime(key, period, now = /* @__PURE__ */ new Date()) {
|
|
9591
|
+
const shown = read();
|
|
9592
|
+
const today = now.toISOString().slice(0, 10);
|
|
9593
|
+
const last = shown[key];
|
|
9594
|
+
if (last && (period === "install" || last === today))
|
|
9595
|
+
return false;
|
|
9596
|
+
try {
|
|
9597
|
+
(0, import_node_fs8.writeFileSync)(file(), JSON.stringify({ ...shown, [key]: today }));
|
|
9598
|
+
} catch {
|
|
9599
|
+
}
|
|
9600
|
+
return true;
|
|
9601
|
+
}
|
|
9602
|
+
|
|
7494
9603
|
// src/setup.ts
|
|
7495
|
-
var
|
|
7496
|
-
var
|
|
7497
|
-
var
|
|
9604
|
+
var import_node_child_process5 = require("node:child_process");
|
|
9605
|
+
var import_node_fs9 = require("node:fs");
|
|
9606
|
+
var import_node_path10 = require("node:path");
|
|
7498
9607
|
var POLICY_NAME = "LocalNetworkAccessAllowedForUrls";
|
|
7499
9608
|
var POLICY_URL = "https://tuttidaw.com";
|
|
7500
9609
|
var exec = (command, args) => {
|
|
7501
|
-
const r2 = (0,
|
|
9610
|
+
const r2 = (0, import_node_child_process5.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
|
|
7502
9611
|
return { status: r2.status, stdout: r2.stdout ?? "" };
|
|
7503
9612
|
};
|
|
7504
9613
|
var WINDOWS_KEYS = [
|
|
@@ -7613,16 +9722,16 @@ function policySteps(undo, platform = process.platform, run = exec) {
|
|
|
7613
9722
|
};
|
|
7614
9723
|
});
|
|
7615
9724
|
return LINUX_POLICY_FILES.map(
|
|
7616
|
-
(
|
|
7617
|
-
describe: `delete ${
|
|
9725
|
+
(file2) => undo ? {
|
|
9726
|
+
describe: `delete ${file2}`,
|
|
7618
9727
|
run() {
|
|
7619
|
-
(0,
|
|
9728
|
+
(0, import_node_fs9.rmSync)(file2, { force: true });
|
|
7620
9729
|
}
|
|
7621
9730
|
} : {
|
|
7622
|
-
describe: `write ${
|
|
9731
|
+
describe: `write ${file2} (${POLICY_NAME}: ["${POLICY_URL}"])`,
|
|
7623
9732
|
run() {
|
|
7624
|
-
(0,
|
|
7625
|
-
(0,
|
|
9733
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path10.dirname)(file2), { recursive: true });
|
|
9734
|
+
(0, import_node_fs9.writeFileSync)(file2, linuxPolicyJson());
|
|
7626
9735
|
}
|
|
7627
9736
|
}
|
|
7628
9737
|
);
|
|
@@ -7638,22 +9747,22 @@ for your user. It allows nothing else, and you can see it at chrome://policy
|
|
|
7638
9747
|
(edge://policy). Undo any time with: tutticode setup --undo`;
|
|
7639
9748
|
|
|
7640
9749
|
// src/skills.ts
|
|
7641
|
-
var
|
|
7642
|
-
var
|
|
7643
|
-
var
|
|
7644
|
-
function targetDir(target, home = (0,
|
|
7645
|
-
return (0,
|
|
9750
|
+
var import_node_fs10 = require("node:fs");
|
|
9751
|
+
var import_node_os5 = require("node:os");
|
|
9752
|
+
var import_node_path11 = require("node:path");
|
|
9753
|
+
function targetDir(target, home = (0, import_node_os5.homedir)()) {
|
|
9754
|
+
return (0, import_node_path11.join)(home, `.${target}`, "skills");
|
|
7646
9755
|
}
|
|
7647
9756
|
function shippedSkills(dir = skillsDir()) {
|
|
7648
|
-
if (!(0,
|
|
9757
|
+
if (!(0, import_node_fs10.existsSync)(dir))
|
|
7649
9758
|
return [];
|
|
7650
|
-
return (0,
|
|
7651
|
-
(name) => (0,
|
|
9759
|
+
return (0, import_node_fs10.readdirSync)(dir).filter(
|
|
9760
|
+
(name) => (0, import_node_fs10.statSync)((0, import_node_path11.join)(dir, name)).isDirectory() && (0, import_node_fs10.existsSync)((0, import_node_path11.join)(dir, name, "SKILL.md"))
|
|
7652
9761
|
).sort();
|
|
7653
9762
|
}
|
|
7654
|
-
function defaultTargets(home = (0,
|
|
9763
|
+
function defaultTargets(home = (0, import_node_os5.homedir)()) {
|
|
7655
9764
|
const found = ["claude", "copilot", "codex"].filter(
|
|
7656
|
-
(t) => (0,
|
|
9765
|
+
(t) => (0, import_node_fs10.existsSync)((0, import_node_path11.join)(home, `.${t}`))
|
|
7657
9766
|
);
|
|
7658
9767
|
return found.length ? [...found] : ["claude"];
|
|
7659
9768
|
}
|
|
@@ -7664,9 +9773,9 @@ function installSkills(destinations, source = skillsDir()) {
|
|
|
7664
9773
|
const installed = [];
|
|
7665
9774
|
for (const destination of destinations) {
|
|
7666
9775
|
for (const skill of skills) {
|
|
7667
|
-
const path = (0,
|
|
7668
|
-
(0,
|
|
7669
|
-
(0,
|
|
9776
|
+
const path = (0, import_node_path11.resolve)(destination, skill);
|
|
9777
|
+
(0, import_node_fs10.rmSync)(path, { recursive: true, force: true });
|
|
9778
|
+
(0, import_node_fs10.cpSync)((0, import_node_path11.join)(source, skill), path, { recursive: true });
|
|
7670
9779
|
installed.push({ skill, path });
|
|
7671
9780
|
}
|
|
7672
9781
|
}
|
|
@@ -7909,7 +10018,19 @@ var afrobeats = {
|
|
|
7909
10018
|
p90: [-8.9, -1.9, -2.9, -4.9, -6.9, -8.9, -9.9, -11.9, -12.9, -17.9],
|
|
7910
10019
|
source: TONAL_BALANCE_SOURCE
|
|
7911
10020
|
},
|
|
7912
|
-
sidechain:
|
|
10021
|
+
sidechain: {
|
|
10022
|
+
ducks: [
|
|
10023
|
+
{
|
|
10024
|
+
trigger: "kick",
|
|
10025
|
+
targets: ["bass"],
|
|
10026
|
+
ratio: 3,
|
|
10027
|
+
attackMs: 1,
|
|
10028
|
+
releaseMs: 80,
|
|
10029
|
+
depthDb: 2
|
|
10030
|
+
}
|
|
10031
|
+
],
|
|
10032
|
+
note: "Clears the kick, no pumping; keep the bounce."
|
|
10033
|
+
},
|
|
7913
10034
|
reverb: "EQ Eight into light Compressor on vocal; Reverb short room for percussion and a darker plate for voice.",
|
|
7914
10035
|
delay: "Echo dotted eighth on guitar or phrase endings; filter repeats away from the lead vocal.",
|
|
7915
10036
|
stereo: "Center kick, bass and lead vocal; spread the guitar and shaker without widening the sub.",
|
|
@@ -8694,7 +10815,19 @@ var boomBap = {
|
|
|
8694
10815
|
p90: [-12.2, -3.2, -2.2, -4.2, -6.2, -8.2, -9.2, -12.2, -16.2, -23.2],
|
|
8695
10816
|
source: TONAL_BALANCE_SOURCE
|
|
8696
10817
|
},
|
|
8697
|
-
sidechain:
|
|
10818
|
+
sidechain: {
|
|
10819
|
+
ducks: [
|
|
10820
|
+
{
|
|
10821
|
+
trigger: "kick",
|
|
10822
|
+
targets: ["bass"],
|
|
10823
|
+
ratio: 3,
|
|
10824
|
+
attackMs: 1,
|
|
10825
|
+
releaseMs: 70,
|
|
10826
|
+
depthDb: 2
|
|
10827
|
+
}
|
|
10828
|
+
],
|
|
10829
|
+
note: "Clears the kick, no EDM pumping."
|
|
10830
|
+
},
|
|
8698
10831
|
reverb: "Redux lightly on the sample, Saturator for body, EQ Eight to trim hiss; short Reverb on snare, rap mostly dry.",
|
|
8699
10832
|
delay: "Echo throws on selected rhyme endings, not across the whole verse.",
|
|
8700
10833
|
stereo: "Centered kick/snare/bass/vocal; sample can be narrow with wider hook answers.",
|
|
@@ -9201,11 +11334,11 @@ var cinematic = {
|
|
|
9201
11334
|
p90: [-11.4, -4.4, -3.4, -3.4, -5.4, -7.4, -8.4, -10.4, -12.4, -18.4],
|
|
9202
11335
|
source: TONAL_BALANCE_SOURCE
|
|
9203
11336
|
},
|
|
9204
|
-
sidechain:
|
|
11337
|
+
sidechain: null,
|
|
9205
11338
|
reverb: "EQ Eight before Hybrid Reverb keeps the hall clear; different predelays establish near piano and distant orchestra.",
|
|
9206
11339
|
delay: "Echo can extend a pulse, but sync it so the ostinato does not become an accidental flam.",
|
|
9207
11340
|
stereo: "Strings and upper brass broad, low drums/bass centered; keep dialogue space in the middle when scoring picture.",
|
|
9208
|
-
lowEnd: "Saturator on low-drum substitutes adds weight; highpass reverb. Limiter catches isolated impacts, not the entire crescendo. Delivery loudness follows the
|
|
11341
|
+
lowEnd: "Saturator on low-drum substitutes adds weight; highpass reverb. Thin pad/sub under impacts, no dance pumping. Limiter catches isolated impacts, not the entire crescendo. Delivery loudness follows the brief."
|
|
9209
11342
|
},
|
|
9210
11343
|
arrangement: {
|
|
9211
11344
|
variationEveryBars: 4,
|
|
@@ -10556,7 +12689,7 @@ var disco = {
|
|
|
10556
12689
|
p90: [-12.1, -3.1, -2.1, -5.1, -7.1, -8.1, -9.1, -10.1, -11.1, -17.1],
|
|
10557
12690
|
source: TONAL_BALANCE_SOURCE
|
|
10558
12691
|
},
|
|
10559
|
-
sidechain:
|
|
12692
|
+
sidechain: null,
|
|
10560
12693
|
reverb: "Reverb plate on strings and vocal, small room on drums.",
|
|
10561
12694
|
delay: "Quiet Echo slap on vocal; avoid busy feedback against guitar sixteenths.",
|
|
10562
12695
|
stereo: "Strings wide, rhythm guitar to one side with keys answering; bass and kick centered.",
|
|
@@ -10875,14 +13008,34 @@ var drumAndBass = {
|
|
|
10875
13008
|
"Climbing Keys G Minor 140 bpm"
|
|
10876
13009
|
],
|
|
10877
13010
|
mix: {
|
|
10878
|
-
lufs: { min: -
|
|
13011
|
+
lufs: { min: -9, max: -7 },
|
|
10879
13012
|
tonalBalance: {
|
|
10880
13013
|
p10: [-14.5, -9, -9.5, -12.5, -15, -16, -16.5, -17, -19, -25.5],
|
|
10881
13014
|
p50: [-9.5, -5, -6.5, -9.5, -12, -13, -13.5, -14, -15, -20.5],
|
|
10882
13015
|
p90: [-4.5, -1, -3.5, -6.5, -9, -10, -10.5, -11, -11, -15.5],
|
|
10883
13016
|
source: TONAL_BALANCE_SOURCE
|
|
10884
13017
|
},
|
|
10885
|
-
sidechain:
|
|
13018
|
+
sidechain: {
|
|
13019
|
+
ducks: [
|
|
13020
|
+
{
|
|
13021
|
+
trigger: "kick",
|
|
13022
|
+
targets: ["bass", "pad"],
|
|
13023
|
+
ratio: 4,
|
|
13024
|
+
attackMs: 0.5,
|
|
13025
|
+
releaseMs: 100,
|
|
13026
|
+
depthDb: 4
|
|
13027
|
+
},
|
|
13028
|
+
{
|
|
13029
|
+
trigger: "snare",
|
|
13030
|
+
targets: ["pad"],
|
|
13031
|
+
ratio: 3,
|
|
13032
|
+
attackMs: 0.5,
|
|
13033
|
+
releaseMs: 120,
|
|
13034
|
+
depthDb: 3
|
|
13035
|
+
}
|
|
13036
|
+
],
|
|
13037
|
+
note: "Pads duck to kick and snare; the sub only to the kick."
|
|
13038
|
+
},
|
|
10886
13039
|
reverb: "Short, bright snare reverb; big wash on breakdown pads/vocals.",
|
|
10887
13040
|
delay: "1/4 or 1/8 dotted on vocals; delay throws into drops.",
|
|
10888
13041
|
stereo: "Sub and kick mono, reese mids wide, breaks slightly wide, pads very wide.",
|
|
@@ -11157,7 +13310,7 @@ var dubstep = {
|
|
|
11157
13310
|
},
|
|
11158
13311
|
starterClips: ["Dub Halftime 136 bpm", "Halftime Pattern 136 bpm"],
|
|
11159
13312
|
mix: {
|
|
11160
|
-
lufs: { min: -10, max: -
|
|
13313
|
+
lufs: { min: -10, max: -8 },
|
|
11161
13314
|
tonalBalance: {
|
|
11162
13315
|
p10: [
|
|
11163
13316
|
-11.1,
|
|
@@ -11175,7 +13328,19 @@ var dubstep = {
|
|
|
11175
13328
|
p90: [-1.1, -0.1, -5.1, -9.1, -11.1, -11.1, -11.1, -13.1, -15.1, -21.1],
|
|
11176
13329
|
source: TONAL_BALANCE_SOURCE
|
|
11177
13330
|
},
|
|
11178
|
-
sidechain:
|
|
13331
|
+
sidechain: {
|
|
13332
|
+
ducks: [
|
|
13333
|
+
{
|
|
13334
|
+
trigger: "kick",
|
|
13335
|
+
targets: ["bass"],
|
|
13336
|
+
ratio: 4,
|
|
13337
|
+
attackMs: 0.5,
|
|
13338
|
+
releaseMs: 60,
|
|
13339
|
+
depthDb: 4
|
|
13340
|
+
}
|
|
13341
|
+
],
|
|
13342
|
+
note: "The sub ducks briefly for the kick only."
|
|
13343
|
+
},
|
|
11179
13344
|
reverb: "Dark Reverb on snare and isolated bells; no reverb on the clean sub.",
|
|
11180
13345
|
delay: "Dub-style Echo throws into the gaps, filtered and automated before the next bass call.",
|
|
11181
13346
|
stereo: "Sub and kick mono; widen only the upper bass layer and atmospheric answers.",
|
|
@@ -11820,7 +13985,7 @@ var funk = {
|
|
|
11820
13985
|
p90: [-13.5, -3.5, -2.5, -4.5, -6.5, -7.5, -8.5, -10.5, -12.5, -19.5],
|
|
11821
13986
|
source: TONAL_BALANCE_SOURCE
|
|
11822
13987
|
},
|
|
11823
|
-
sidechain:
|
|
13988
|
+
sidechain: null,
|
|
11824
13989
|
reverb: "Short Reverb room; keep guitar scratches and bass dry.",
|
|
11825
13990
|
delay: "Brief Echo slap on a horn or vocal answer only.",
|
|
11826
13991
|
stereo: "Bass, snare and lead centered; guitar and clav answer from opposite sides.",
|
|
@@ -11837,7 +14002,7 @@ var funk = {
|
|
|
11837
14002
|
{
|
|
11838
14003
|
kind: "breakdown",
|
|
11839
14004
|
where: "Middle",
|
|
11840
|
-
how: "Expose drums and bass; reintroduce clav before horns."
|
|
14005
|
+
how: "Expose the drums (and bass, in a bass feature); reintroduce clav before horns."
|
|
11841
14006
|
},
|
|
11842
14007
|
{
|
|
11843
14008
|
kind: "variation",
|
|
@@ -12083,7 +14248,7 @@ var futureBass = {
|
|
|
12083
14248
|
},
|
|
12084
14249
|
starterClips: [],
|
|
12085
14250
|
mix: {
|
|
12086
|
-
lufs: { min: -10, max: -
|
|
14251
|
+
lufs: { min: -10, max: -8 },
|
|
12087
14252
|
tonalBalance: {
|
|
12088
14253
|
p10: [
|
|
12089
14254
|
-17.8,
|
|
@@ -12101,7 +14266,27 @@ var futureBass = {
|
|
|
12101
14266
|
p90: [-7.8, -1.8, -4.8, -6.8, -6.8, -6.8, -7.8, -8.8, -9.8, -14.8],
|
|
12102
14267
|
source: TONAL_BALANCE_SOURCE
|
|
12103
14268
|
},
|
|
12104
|
-
sidechain:
|
|
14269
|
+
sidechain: {
|
|
14270
|
+
ducks: [
|
|
14271
|
+
{
|
|
14272
|
+
trigger: "kick",
|
|
14273
|
+
targets: ["chords", "pad"],
|
|
14274
|
+
ratio: 8,
|
|
14275
|
+
attackMs: 0.5,
|
|
14276
|
+
releaseMs: 200,
|
|
14277
|
+
depthDb: 8
|
|
14278
|
+
},
|
|
14279
|
+
{
|
|
14280
|
+
trigger: "kick",
|
|
14281
|
+
targets: ["bass"],
|
|
14282
|
+
ratio: 4,
|
|
14283
|
+
attackMs: 0.5,
|
|
14284
|
+
releaseMs: 80,
|
|
14285
|
+
depthDb: 4
|
|
14286
|
+
}
|
|
14287
|
+
],
|
|
14288
|
+
note: "Chord stacks pump hard, release follows the swell; the sub duck is shorter."
|
|
14289
|
+
},
|
|
12105
14290
|
reverb: "EQ Eight clears chord low mids, Saturator adds body, Chorus-Ensemble widens upper layers, then Reverb; keep dry transients in front.",
|
|
12106
14291
|
delay: "Echo dotted eighth on vocal chops; automate throws into gaps rather than washing out the next chord.",
|
|
12107
14292
|
stereo: "Wide chords and chops above a centered sub/kick/snare; mono-check detuned layers.",
|
|
@@ -12373,7 +14558,19 @@ var gospel = {
|
|
|
12373
14558
|
p90: [-13.1, -5.1, -4.1, -4.1, -5.1, -6.1, -7.1, -9.1, -11.1, -17.1],
|
|
12374
14559
|
source: TONAL_BALANCE_SOURCE
|
|
12375
14560
|
},
|
|
12376
|
-
sidechain:
|
|
14561
|
+
sidechain: {
|
|
14562
|
+
ducks: [
|
|
14563
|
+
{
|
|
14564
|
+
trigger: "kick",
|
|
14565
|
+
targets: ["bass"],
|
|
14566
|
+
ratio: 3,
|
|
14567
|
+
attackMs: 1,
|
|
14568
|
+
releaseMs: 80,
|
|
14569
|
+
depthDb: 1.5
|
|
14570
|
+
}
|
|
14571
|
+
],
|
|
14572
|
+
note: "Only where bass masks the kick; no dance pumping."
|
|
14573
|
+
},
|
|
12377
14574
|
reverb: "EQ Eight then Compressor on lead/choir; shared Reverb hall with enough predelay to keep consonants clear.",
|
|
12378
14575
|
delay: "Low Echo throws behind the leader; do not repeat over the choir response.",
|
|
12379
14576
|
stereo: "Lead center, choir spread, organ behind the words; retain mono bass.",
|
|
@@ -12427,7 +14624,7 @@ var guofengPop = {
|
|
|
12427
14624
|
feel: {
|
|
12428
14625
|
kind: "straight",
|
|
12429
14626
|
swingPercent: 50,
|
|
12430
|
-
note: "Soft even-16th R&B-pop beat; the Chinese colour
|
|
14627
|
+
note: "Soft even-16th R&B-pop beat; the Chinese colour is in the melody (pentatonic, slides, grace notes) and woodblock-like rim."
|
|
12431
14628
|
},
|
|
12432
14629
|
keys: ["D major", "E major", "G major", "B minor", "E minor"],
|
|
12433
14630
|
forms: [
|
|
@@ -12732,7 +14929,7 @@ var guofengPop = {
|
|
|
12732
14929
|
reverb: "Large hall on flute, zither and vocal (the misty space is the style), pre-delay for clear words; drums dry.",
|
|
12733
14930
|
delay: "Dotted-1/8 Echo on flute and plucks; vocal throws on line ends.",
|
|
12734
14931
|
stereo: "Zither and strings wide, flute off-centre, vocal centred on top.",
|
|
12735
|
-
lowEnd: "Soft and round; high-pass plucks and flute ~150 Hz. Chain: vocal EQ Eight > Compressor > Hybrid Reverb; master Glue
|
|
14932
|
+
lowEnd: "Soft and round; high-pass plucks and flute ~150 Hz. Chain: vocal EQ Eight > Compressor > Hybrid Reverb; gentle master Glue."
|
|
12736
14933
|
},
|
|
12737
14934
|
arrangement: {
|
|
12738
14935
|
variationEveryBars: 8,
|
|
@@ -12760,7 +14957,7 @@ var guofengPop = {
|
|
|
12760
14957
|
]
|
|
12761
14958
|
},
|
|
12762
14959
|
references: ["Jay Chou", "JJ Lin", "Silence Wang", "Henry Huo", "Isaac Hong"],
|
|
12763
|
-
why: "Guofeng is C-pop with a Chinese accent:
|
|
14960
|
+
why: "Guofeng is C-pop with a Chinese accent: pop/R&B beat and chords under an ornamented pentatonic melody and classical imagery. No Chinese instrument samples yet: Koto / Eastern Harp stand in for guzheng, Flute Mellow / Indian Flute for dizi, Violin Strings with slides for erhu."
|
|
12764
14961
|
};
|
|
12765
14962
|
|
|
12766
14963
|
// ../../src/components/editor-ai/style/cards/house.ts
|
|
@@ -13058,14 +15255,34 @@ var house = {
|
|
|
13058
15255
|
"House 90s C Minor 128 bpm"
|
|
13059
15256
|
],
|
|
13060
15257
|
mix: {
|
|
13061
|
-
lufs: { min: -
|
|
15258
|
+
lufs: { min: -10, max: -8 },
|
|
13062
15259
|
tonalBalance: {
|
|
13063
15260
|
p10: [-15.5, -8.5, -9, -12, -14.5, -16, -17, -18, -20, -26.5],
|
|
13064
15261
|
p50: [-10.5, -4.5, -6, -9, -11.5, -13, -14, -15, -16, -21.5],
|
|
13065
15262
|
p90: [-5.5, -0.5, -3, -6, -8.5, -10, -11, -12, -12, -16.5],
|
|
13066
15263
|
source: TONAL_BALANCE_SOURCE
|
|
13067
15264
|
},
|
|
13068
|
-
sidechain:
|
|
15265
|
+
sidechain: {
|
|
15266
|
+
ducks: [
|
|
15267
|
+
{
|
|
15268
|
+
trigger: "kick",
|
|
15269
|
+
targets: ["chords", "pad"],
|
|
15270
|
+
ratio: 4,
|
|
15271
|
+
attackMs: 1,
|
|
15272
|
+
releaseMs: 200,
|
|
15273
|
+
depthDb: 5
|
|
15274
|
+
},
|
|
15275
|
+
{
|
|
15276
|
+
trigger: "kick",
|
|
15277
|
+
targets: ["bass"],
|
|
15278
|
+
ratio: 3,
|
|
15279
|
+
attackMs: 1,
|
|
15280
|
+
releaseMs: 80,
|
|
15281
|
+
depthDb: 2
|
|
15282
|
+
}
|
|
15283
|
+
],
|
|
15284
|
+
note: "Chords/pads pump (release ~1/8); the offbeat bass only lightly."
|
|
15285
|
+
},
|
|
13069
15286
|
reverb: "Short plate on clap, large hall on breakdown pads/vocals.",
|
|
13070
15287
|
delay: "1/8 dotted or 3/16 on stabs and vocals, filtered.",
|
|
13071
15288
|
stereo: "Kick, bass, clap centered; hats/shaker/perc spread wide.",
|
|
@@ -13697,7 +15914,7 @@ var jpopAnime = {
|
|
|
13697
15914
|
"Downwards A Minor 141 bpm"
|
|
13698
15915
|
],
|
|
13699
15916
|
mix: {
|
|
13700
|
-
lufs: { min: -
|
|
15917
|
+
lufs: { min: -10, max: -8 },
|
|
13701
15918
|
tonalBalance: {
|
|
13702
15919
|
p10: [-20.5, -12, -9.5, -10.5, -12, -13, -14, -15, -18, -26],
|
|
13703
15920
|
p50: [-15.5, -8, -6.5, -7.5, -9, -10, -11, -12, -14, -21],
|
|
@@ -13708,7 +15925,7 @@ var jpopAnime = {
|
|
|
13708
15925
|
reverb: "Bright plate on vocal and snare, hall on strings.",
|
|
13709
15926
|
delay: "Dotted-1/8 vocal delay tucked under.",
|
|
13710
15927
|
stereo: "Doubled guitars hard L/R, strings wide, vocal centred on top.",
|
|
13711
|
-
lowEnd: "Tight kick and bass; high-pass guitars ~100 Hz. Chain: guitars Amp > Cabinet; vocal Compressor > EQ Eight > Reverb; master Glue Compressor > Limiter
|
|
15928
|
+
lowEnd: "Tight kick and bass; high-pass guitars ~100 Hz. Chain: guitars Amp > Cabinet; vocal Compressor > EQ Eight > Reverb; master Glue Compressor > one Limiter (ceiling -1 dBTP)."
|
|
13712
15929
|
},
|
|
13713
15930
|
arrangement: {
|
|
13714
15931
|
variationEveryBars: 4,
|
|
@@ -13766,19 +15983,19 @@ var kpop = {
|
|
|
13766
15983
|
"\uCF00\uC774\uD31D",
|
|
13767
15984
|
"\uC544\uC774\uB3CC"
|
|
13768
15985
|
],
|
|
13769
|
-
summary: "Idol dance-pop, 95-130 BPM, genre per section: rap verse,
|
|
15986
|
+
summary: "Idol dance-pop, 95-130 BPM, genre per section: rap verse, build, EDM/trap chorus, dance break, key-up end.",
|
|
13770
15987
|
tempo: { min: 95, max: 130, typical: 120 },
|
|
13771
15988
|
meter: "4/4",
|
|
13772
15989
|
feel: {
|
|
13773
15990
|
kind: "straight",
|
|
13774
15991
|
swingPercent: 50,
|
|
13775
|
-
note: "Quantized
|
|
15992
|
+
note: "Quantized; trap parts half-time with 32nd hat rolls."
|
|
13776
15993
|
},
|
|
13777
15994
|
keys: ["F# minor", "C# minor", "G minor", "A minor", "Eb major"],
|
|
13778
15995
|
forms: [
|
|
13779
15996
|
{
|
|
13780
15997
|
id: "title-track",
|
|
13781
|
-
name: "Title track
|
|
15998
|
+
name: "Title track, key-up end",
|
|
13782
15999
|
sections: [
|
|
13783
16000
|
{
|
|
13784
16001
|
name: "Intro",
|
|
@@ -13798,9 +16015,9 @@ var kpop = {
|
|
|
13798
16015
|
{
|
|
13799
16016
|
name: "Pre-Chorus",
|
|
13800
16017
|
bars: 8,
|
|
13801
|
-
energy: 0.
|
|
16018
|
+
energy: 0.55,
|
|
13802
16019
|
roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
|
|
13803
|
-
drums: "
|
|
16020
|
+
drums: "build",
|
|
13804
16021
|
bass: "pulse"
|
|
13805
16022
|
},
|
|
13806
16023
|
{
|
|
@@ -13830,9 +16047,9 @@ var kpop = {
|
|
|
13830
16047
|
{
|
|
13831
16048
|
name: "Pre-Chorus",
|
|
13832
16049
|
bars: 8,
|
|
13833
|
-
energy: 0.
|
|
16050
|
+
energy: 0.6,
|
|
13834
16051
|
roles: ["drums", "bass", "chords", "pad", "vocal", "fx"],
|
|
13835
|
-
drums: "
|
|
16052
|
+
drums: "build",
|
|
13836
16053
|
bass: "pulse"
|
|
13837
16054
|
},
|
|
13838
16055
|
{
|
|
@@ -13878,7 +16095,7 @@ var kpop = {
|
|
|
13878
16095
|
},
|
|
13879
16096
|
{
|
|
13880
16097
|
id: "beat-switch",
|
|
13881
|
-
name: "Beat switch:
|
|
16098
|
+
name: "Beat switch: trap chorus, dance last chorus",
|
|
13882
16099
|
sections: [
|
|
13883
16100
|
{
|
|
13884
16101
|
name: "Intro",
|
|
@@ -13898,8 +16115,9 @@ var kpop = {
|
|
|
13898
16115
|
{
|
|
13899
16116
|
name: "Pre-Chorus",
|
|
13900
16117
|
bars: 8,
|
|
13901
|
-
energy: 0.
|
|
13902
|
-
roles: ["chords", "pad", "vocal", "fx"]
|
|
16118
|
+
energy: 0.6,
|
|
16119
|
+
roles: ["drums", "chords", "pad", "vocal", "fx"],
|
|
16120
|
+
drums: "build"
|
|
13903
16121
|
},
|
|
13904
16122
|
{
|
|
13905
16123
|
name: "Chorus",
|
|
@@ -13944,6 +16162,7 @@ var kpop = {
|
|
|
13944
16162
|
kick: "X..x..x.X.x.....X..x..x.X.....o.",
|
|
13945
16163
|
snare: "....X.......X.......X.......X...",
|
|
13946
16164
|
clap: "....x.......x.......x.......x...",
|
|
16165
|
+
snap: "....g.......g..o....g.......g...",
|
|
13947
16166
|
hatClosed: "o.x.o.xxo.x.o.x.o.x.o.xxo.x.o...",
|
|
13948
16167
|
hatOpen: "..............................x."
|
|
13949
16168
|
}
|
|
@@ -13977,18 +16196,29 @@ var kpop = {
|
|
|
13977
16196
|
kick: "X..X..X...X.X...",
|
|
13978
16197
|
snare: "....X.......X...",
|
|
13979
16198
|
clap: "....x..x....x..x",
|
|
16199
|
+
snap: "....x.......x...",
|
|
13980
16200
|
hatClosed: "..x...x...x...x."
|
|
13981
16201
|
}
|
|
16202
|
+
},
|
|
16203
|
+
{
|
|
16204
|
+
id: "build",
|
|
16205
|
+
name: "Build: kick out, snare 4ths > 8ths > 16ths",
|
|
16206
|
+
stepsPerBeat: 4,
|
|
16207
|
+
lanes: {
|
|
16208
|
+
snare: "o...o...o...o...x.x.x.x.xxxxXXXX",
|
|
16209
|
+
clap: "....o.......o.......x.......x...",
|
|
16210
|
+
hatClosed: "oooooooooooooooooxoxoxoxxxxxXXXX"
|
|
16211
|
+
}
|
|
13982
16212
|
}
|
|
13983
16213
|
],
|
|
13984
16214
|
fills: [
|
|
13985
16215
|
{
|
|
13986
16216
|
id: "riser-roll",
|
|
13987
16217
|
everyBars: 8,
|
|
13988
|
-
stepsPerBeat:
|
|
13989
|
-
steps:
|
|
13990
|
-
lanes: { snare: "x.x.
|
|
13991
|
-
note: "
|
|
16218
|
+
stepsPerBeat: 8,
|
|
16219
|
+
steps: 24,
|
|
16220
|
+
lanes: { snare: "o.o.x.x.xxxxXXXX........" },
|
|
16221
|
+
note: "Snare roll, kick out, last beat silent."
|
|
13992
16222
|
},
|
|
13993
16223
|
{
|
|
13994
16224
|
id: "drop-gap",
|
|
@@ -14000,8 +16230,8 @@ var kpop = {
|
|
|
14000
16230
|
}
|
|
14001
16231
|
],
|
|
14002
16232
|
rules: [
|
|
14003
|
-
"One groove per section
|
|
14004
|
-
"Stack snare+clap
|
|
16233
|
+
"One groove per section (verse, build, trap, chorus, jersey).",
|
|
16234
|
+
"Stack snare+clap+snap; hits mark choreography."
|
|
14005
16235
|
]
|
|
14006
16236
|
},
|
|
14007
16237
|
bass: {
|
|
@@ -14030,7 +16260,7 @@ var kpop = {
|
|
|
14030
16260
|
velocity: 110
|
|
14031
16261
|
}
|
|
14032
16262
|
],
|
|
14033
|
-
notes: "Synth bass on the dance kick;
|
|
16263
|
+
notes: "Synth bass on the dance kick; 808 in rap/trap."
|
|
14034
16264
|
},
|
|
14035
16265
|
harmony: {
|
|
14036
16266
|
modes: [
|
|
@@ -14049,7 +16279,7 @@ var kpop = {
|
|
|
14049
16279
|
chords: ["i", "bVII", "bVI", "V"],
|
|
14050
16280
|
weight: 2,
|
|
14051
16281
|
barsPerChord: 1,
|
|
14052
|
-
use: "major V
|
|
16282
|
+
use: "major V into the hook"
|
|
14053
16283
|
},
|
|
14054
16284
|
{
|
|
14055
16285
|
chords: ["IVmaj7", "V", "iii7", "vi"],
|
|
@@ -14065,28 +16295,24 @@ var kpop = {
|
|
|
14065
16295
|
}
|
|
14066
16296
|
],
|
|
14067
16297
|
extensions: ["add9", "maj7", "7", "sus4"],
|
|
14068
|
-
voicing: "Close synth/brass stabs
|
|
14069
|
-
harmonicRhythm: "1 chord/bar; rap and dance break vamp
|
|
16298
|
+
voicing: "Close synth/brass stabs; open piano+strings in the bridge.",
|
|
16299
|
+
harmonicRhythm: "1 chord/bar; rap and dance break vamp."
|
|
14070
16300
|
},
|
|
14071
16301
|
melody: {
|
|
14072
16302
|
range: { low: 57, high: 79 },
|
|
14073
16303
|
density: "dense",
|
|
14074
16304
|
notesPerBar: [4, 12],
|
|
14075
|
-
rhythm: "16th verses and rap;
|
|
16305
|
+
rhythm: "16th verses and rap; a short title word repeats in the chorus.",
|
|
14076
16306
|
motifs: [
|
|
14077
16307
|
"One-word title hook repeated 4x",
|
|
14078
16308
|
"Pre-chorus climbs to a held note",
|
|
14079
|
-
"Post-chorus
|
|
16309
|
+
"Post-chorus vocal-chop riff"
|
|
14080
16310
|
]
|
|
14081
16311
|
},
|
|
14082
16312
|
palette: {
|
|
14083
16313
|
drums: {
|
|
14084
|
-
keywords: ["punchy kick", "
|
|
14085
|
-
presets: [
|
|
14086
|
-
"drumrack:Crystal Clear Kit",
|
|
14087
|
-
"drumrack:Street Mind Kit",
|
|
14088
|
-
"drumrack:707 Core Kit"
|
|
14089
|
-
]
|
|
16314
|
+
keywords: ["punchy kick", "snap", "trap hats"],
|
|
16315
|
+
presets: ["drumrack:Street Mind Kit", "drumrack:Clint West Kit"]
|
|
14090
16316
|
},
|
|
14091
16317
|
bass: {
|
|
14092
16318
|
keywords: ["808", "synth bass", "sub"],
|
|
@@ -14106,29 +16332,40 @@ var kpop = {
|
|
|
14106
16332
|
},
|
|
14107
16333
|
lead: {
|
|
14108
16334
|
keywords: ["vocal chop", "pluck", "saw lead"],
|
|
14109
|
-
presets: ["
|
|
16335
|
+
presets: ["sampler:Vocal Chop Ah", "meld:PolyPlucks"]
|
|
14110
16336
|
},
|
|
14111
16337
|
fx: { keywords: ["riser", "impact", "downlifter", "reverse cymbal"] }
|
|
14112
16338
|
},
|
|
14113
16339
|
starterClips: [
|
|
14114
16340
|
"Pop Riddim 124 bpm",
|
|
14115
|
-
"Dancehall Trap 113 bpm",
|
|
14116
16341
|
"LD Core Kit Pop 120 bpm",
|
|
14117
16342
|
"Uplifting Club Vamp F Min 127 bpm"
|
|
14118
16343
|
],
|
|
14119
16344
|
mix: {
|
|
14120
|
-
lufs: { min: -
|
|
16345
|
+
lufs: { min: -11, max: -9 },
|
|
14121
16346
|
tonalBalance: {
|
|
14122
16347
|
p10: [-16.5, -9, -8.5, -11, -13, -14.5, -15.5, -16.5, -19, -26.5],
|
|
14123
16348
|
p50: [-11.5, -5.5, -5.5, -8, -10, -11.5, -12.5, -13.5, -15, -21.5],
|
|
14124
16349
|
p90: [-6.5, -1.5, -2.5, -5, -7, -8.5, -9.5, -10.5, -11, -16.5],
|
|
14125
16350
|
source: TONAL_BALANCE_SOURCE
|
|
14126
16351
|
},
|
|
14127
|
-
sidechain:
|
|
16352
|
+
sidechain: {
|
|
16353
|
+
ducks: [
|
|
16354
|
+
{
|
|
16355
|
+
trigger: "kick",
|
|
16356
|
+
targets: ["pad", "chords", "bass"],
|
|
16357
|
+
ratio: 4,
|
|
16358
|
+
attackMs: 1,
|
|
16359
|
+
releaseMs: 150,
|
|
16360
|
+
depthDb: 5
|
|
16361
|
+
}
|
|
16362
|
+
],
|
|
16363
|
+
note: "Chorus pump; measured dip >= 1 dB."
|
|
16364
|
+
},
|
|
14128
16365
|
reverb: "Bright vocal plate, dry rap; hall only in the bridge.",
|
|
14129
|
-
delay: "Dotted-1/8 throws on hook words
|
|
14130
|
-
stereo: "Wide
|
|
14131
|
-
lowEnd: "Mono sub; Saturator on the 808;
|
|
16366
|
+
delay: "Dotted-1/8 throws on hook words.",
|
|
16367
|
+
stereo: "Wide gang vocals in the chorus; rap centred.",
|
|
16368
|
+
lowEnd: "Mono sub; Saturator on the 808; gentle master Glue, 1 dB true-peak headroom."
|
|
14132
16369
|
},
|
|
14133
16370
|
arrangement: {
|
|
14134
16371
|
variationEveryBars: 4,
|
|
@@ -14136,7 +16373,7 @@ var kpop = {
|
|
|
14136
16373
|
{
|
|
14137
16374
|
kind: "build",
|
|
14138
16375
|
where: "Pre-chorus",
|
|
14139
|
-
how: "Kick out, snare roll, Auto Filter sweep, riser."
|
|
16376
|
+
how: "Kick out (build pattern), snare roll, Auto Filter sweep, riser; 4-6 dB under the chorus; last beat silent."
|
|
14140
16377
|
},
|
|
14141
16378
|
{
|
|
14142
16379
|
kind: "breakdown",
|
|
@@ -14146,7 +16383,7 @@ var kpop = {
|
|
|
14146
16383
|
{
|
|
14147
16384
|
kind: "drop",
|
|
14148
16385
|
where: "Dance break",
|
|
14149
|
-
how: "Instrumental beat switch
|
|
16386
|
+
how: "Instrumental beat switch."
|
|
14150
16387
|
},
|
|
14151
16388
|
{
|
|
14152
16389
|
kind: "key-change",
|
|
@@ -14156,7 +16393,7 @@ var kpop = {
|
|
|
14156
16393
|
]
|
|
14157
16394
|
},
|
|
14158
16395
|
references: ["BTS", "BLACKPINK", "TWICE", "Stray Kids", "NewJeans"],
|
|
14159
|
-
why: "
|
|
16396
|
+
why: "The groove switches genre per section while one short title hook holds the song; builds, stops and the dance break frame the choreography."
|
|
14160
16397
|
};
|
|
14161
16398
|
|
|
14162
16399
|
// ../../src/components/editor-ai/style/cards/lofi-hip-hop.ts
|
|
@@ -14429,7 +16666,19 @@ var lofiHipHop = {
|
|
|
14429
16666
|
p90: [-8.5, -2.5, -2, -4, -6, -8.5, -11.5, -15.5, -19.5, -28.5],
|
|
14430
16667
|
source: TONAL_BALANCE_SOURCE
|
|
14431
16668
|
},
|
|
14432
|
-
sidechain:
|
|
16669
|
+
sidechain: {
|
|
16670
|
+
ducks: [
|
|
16671
|
+
{
|
|
16672
|
+
trigger: "kick",
|
|
16673
|
+
targets: ["keys", "pad"],
|
|
16674
|
+
ratio: 3,
|
|
16675
|
+
attackMs: 5,
|
|
16676
|
+
releaseMs: 180,
|
|
16677
|
+
depthDb: 3
|
|
16678
|
+
}
|
|
16679
|
+
],
|
|
16680
|
+
note: "Gentle breathing: keys and pad dip with the kick."
|
|
16681
|
+
},
|
|
14433
16682
|
reverb: "Small dark room on drums, short plate on keys; low wet levels.",
|
|
14434
16683
|
delay: "Tape-style 1/8 dotted on chops/lead, low feedback, filtered repeats.",
|
|
14435
16684
|
stereo: "Drums mostly mono; keys and textures wide.",
|
|
@@ -14816,7 +17065,19 @@ var mandopopUptempo = {
|
|
|
14816
17065
|
p90: [-8, -2.5, -3, -5, -6.5, -8, -9, -10.5, -11.5, -17],
|
|
14817
17066
|
source: TONAL_BALANCE_SOURCE
|
|
14818
17067
|
},
|
|
14819
|
-
sidechain:
|
|
17068
|
+
sidechain: {
|
|
17069
|
+
ducks: [
|
|
17070
|
+
{
|
|
17071
|
+
trigger: "kick",
|
|
17072
|
+
targets: ["pad", "chords"],
|
|
17073
|
+
ratio: 3,
|
|
17074
|
+
attackMs: 1,
|
|
17075
|
+
releaseMs: 150,
|
|
17076
|
+
depthDb: 3
|
|
17077
|
+
}
|
|
17078
|
+
],
|
|
17079
|
+
note: "Light pump, chorus only."
|
|
17080
|
+
},
|
|
14820
17081
|
reverb: "Plate on vocal and snare; hall only on the breakdown.",
|
|
14821
17082
|
delay: "1/8 vocal echo under the chorus; 1/4 throws.",
|
|
14822
17083
|
stereo: "Vocal loud and centred; doubles wide in the chorus.",
|
|
@@ -14878,7 +17139,7 @@ var modernPop = {
|
|
|
14878
17139
|
feel: {
|
|
14879
17140
|
kind: "straight",
|
|
14880
17141
|
swingPercent: 50,
|
|
14881
|
-
note: "Tight
|
|
17142
|
+
note: "Tight, quantized; swing16-54 on hats only if stiff."
|
|
14882
17143
|
},
|
|
14883
17144
|
keys: ["C major", "G major", "D major", "A minor", "F# minor"],
|
|
14884
17145
|
forms: [
|
|
@@ -15081,13 +17342,13 @@ var modernPop = {
|
|
|
15081
17342
|
stepsPerBeat: 4,
|
|
15082
17343
|
steps: 4,
|
|
15083
17344
|
lanes: {},
|
|
15084
|
-
note: "
|
|
17345
|
+
note: "A silent beat before the chorus."
|
|
15085
17346
|
}
|
|
15086
17347
|
],
|
|
15087
17348
|
rules: [
|
|
15088
17349
|
"Pre-chorus builds: snare 8ths -> 16ths over the last 2 bars, or pull the kick out.",
|
|
15089
17350
|
"Chorus adds a layer the verse lacks (claps, open hats, tambourine).",
|
|
15090
|
-
"
|
|
17351
|
+
"Tight, consistent velocities; punchy, sample-replaced."
|
|
15091
17352
|
]
|
|
15092
17353
|
},
|
|
15093
17354
|
bass: {
|
|
@@ -15109,7 +17370,7 @@ var modernPop = {
|
|
|
15109
17370
|
velocity: 104
|
|
15110
17371
|
}
|
|
15111
17372
|
],
|
|
15112
|
-
notes: "Synth or picked bass doubling the kick
|
|
17373
|
+
notes: "Synth or picked bass doubling the chorus kick; roots, octave pops for lift."
|
|
15113
17374
|
},
|
|
15114
17375
|
harmony: {
|
|
15115
17376
|
modes: [
|
|
@@ -15145,7 +17406,7 @@ var modernPop = {
|
|
|
15145
17406
|
}
|
|
15146
17407
|
],
|
|
15147
17408
|
extensions: ["add9", "sus2", "sus4", "maj7"],
|
|
15148
|
-
voicing: "Open triads with
|
|
17409
|
+
voicing: "Open triads with add9; pads hold, piano/guitar pulses 8ths; chorus voicing an octave up.",
|
|
15149
17410
|
harmonicRhythm: "1 chord per bar; same loop through verse and chorus, with energy doing the work."
|
|
15150
17411
|
},
|
|
15151
17412
|
melody: {
|
|
@@ -15214,11 +17475,23 @@ var modernPop = {
|
|
|
15214
17475
|
p90: [-7.5, -2, -2.5, -5, -7, -8.5, -10, -11.5, -12.5, -18.5],
|
|
15215
17476
|
source: TONAL_BALANCE_SOURCE
|
|
15216
17477
|
},
|
|
15217
|
-
sidechain:
|
|
17478
|
+
sidechain: {
|
|
17479
|
+
ducks: [
|
|
17480
|
+
{
|
|
17481
|
+
trigger: "kick",
|
|
17482
|
+
targets: ["pad", "chords"],
|
|
17483
|
+
ratio: 3,
|
|
17484
|
+
attackMs: 1,
|
|
17485
|
+
releaseMs: 150,
|
|
17486
|
+
depthDb: 3
|
|
17487
|
+
}
|
|
17488
|
+
],
|
|
17489
|
+
note: "Light pump in dance-pop; none in ballads."
|
|
17490
|
+
},
|
|
15218
17491
|
reverb: "Vocal plate, short room on drums; long reverb only as throws.",
|
|
15219
17492
|
delay: "1/4 and 1/8 vocal throws at phrase ends.",
|
|
15220
|
-
stereo: "Doubles
|
|
15221
|
-
lowEnd: "Kick and bass locked; highpass
|
|
17493
|
+
stereo: "Doubles and synths wide in the chorus, narrower verse; low end mono.",
|
|
17494
|
+
lowEnd: "Kick and bass locked; highpass the rest at 100-150 Hz."
|
|
15222
17495
|
},
|
|
15223
17496
|
arrangement: {
|
|
15224
17497
|
variationEveryBars: 8,
|
|
@@ -15257,7 +17530,7 @@ var modernPop = {
|
|
|
15257
17530
|
"The Weeknd",
|
|
15258
17531
|
"Ed Sheeran"
|
|
15259
17532
|
],
|
|
15260
|
-
why: "
|
|
17533
|
+
why: "One memorable hook over barely-changing chords; the arc comes from contrast: sparse verses, a building pre-chorus, a louder, wider, higher chorus, something new every 8 bars."
|
|
15261
17534
|
};
|
|
15262
17535
|
|
|
15263
17536
|
// ../../src/components/editor-ai/style/cards/neo-soul.ts
|
|
@@ -15487,7 +17760,19 @@ var neoSoul = {
|
|
|
15487
17760
|
p90: [-12.5, -3.5, -2.5, -3.5, -5.5, -8.5, -10.5, -13.5, -16.5, -23.5],
|
|
15488
17761
|
source: TONAL_BALANCE_SOURCE
|
|
15489
17762
|
},
|
|
15490
|
-
sidechain:
|
|
17763
|
+
sidechain: {
|
|
17764
|
+
ducks: [
|
|
17765
|
+
{
|
|
17766
|
+
trigger: "kick",
|
|
17767
|
+
targets: ["bass"],
|
|
17768
|
+
ratio: 3,
|
|
17769
|
+
attackMs: 1,
|
|
17770
|
+
releaseMs: 80,
|
|
17771
|
+
depthDb: 1.5
|
|
17772
|
+
}
|
|
17773
|
+
],
|
|
17774
|
+
note: "Only if held bass masks the kick; no audible pumping."
|
|
17775
|
+
},
|
|
15491
17776
|
reverb: "Short dark Reverb room around the band; longer vocal plate audible only between phrases.",
|
|
15492
17777
|
delay: "Filtered Echo throws on selected vocal endings.",
|
|
15493
17778
|
stereo: "Keep lead and bass close and centered; electric piano medium-wide, backing vocals wider.",
|
|
@@ -15759,7 +18044,7 @@ var reggae = {
|
|
|
15759
18044
|
p90: [-9.1, -1.1, -2.1, -5.1, -8.1, -10.1, -12.1, -14.1, -17.1, -23.1],
|
|
15760
18045
|
source: TONAL_BALANCE_SOURCE
|
|
15761
18046
|
},
|
|
15762
|
-
sidechain:
|
|
18047
|
+
sidechain: null,
|
|
15763
18048
|
reverb: "Reverb on rim/organ for a short spring-like splash; EQ Eight cuts lows on the wet return.",
|
|
15764
18049
|
delay: "Echo filtered tape-style throws; automate sends on isolated words or rim hits, then mute the dry source for dub space.",
|
|
15765
18050
|
stereo: "Bass, kick and vocal centered; skank/organ apart; dub returns may travel wider.",
|
|
@@ -16023,7 +18308,19 @@ var reggaeton = {
|
|
|
16023
18308
|
p90: [-6.6, -0.6, -3.6, -6.6, -8.6, -9.6, -9.6, -10.6, -11.6, -16.6],
|
|
16024
18309
|
source: TONAL_BALANCE_SOURCE
|
|
16025
18310
|
},
|
|
16026
|
-
sidechain:
|
|
18311
|
+
sidechain: {
|
|
18312
|
+
ducks: [
|
|
18313
|
+
{
|
|
18314
|
+
trigger: "kick",
|
|
18315
|
+
targets: ["bass"],
|
|
18316
|
+
ratio: 4,
|
|
18317
|
+
attackMs: 0.5,
|
|
18318
|
+
releaseMs: 60,
|
|
18319
|
+
depthDb: 3
|
|
18320
|
+
}
|
|
18321
|
+
],
|
|
18322
|
+
note: "Released before the next tresillo accent."
|
|
18323
|
+
},
|
|
16027
18324
|
reverb: "EQ Eight and Compressor keep the vocal forward; short Reverb on snare, little on bass.",
|
|
16028
18325
|
delay: "Echo on hook endings with filtered repeats, ducked or muted under the next line.",
|
|
16029
18326
|
stereo: "Center kick, snare, sub and lead; widen supporting plucks and vocal doubles only.",
|
|
@@ -16072,7 +18369,7 @@ var rnb = {
|
|
|
16072
18369
|
kind: "swing",
|
|
16073
18370
|
swingPercent: 57,
|
|
16074
18371
|
grooveId: "swing16-57",
|
|
16075
|
-
note: "Laid back: swung 16th hats and ghosts, snare a hair late, kick on the grid
|
|
18372
|
+
note: "Laid back: swung 16th hats and ghosts, snare a hair late, kick on the grid (double-time, snare on 3: same feel)."
|
|
16076
18373
|
},
|
|
16077
18374
|
keys: ["Eb major", "Ab major", "Db major", "F minor", "C minor"],
|
|
16078
18375
|
forms: [
|
|
@@ -16213,7 +18510,7 @@ var rnb = {
|
|
|
16213
18510
|
stepsPerBeat: 4,
|
|
16214
18511
|
lanes: {
|
|
16215
18512
|
kick: "X.........x.....",
|
|
16216
|
-
|
|
18513
|
+
snap: "....x.......x...",
|
|
16217
18514
|
hatClosed: "x..x..x.o..x..x."
|
|
16218
18515
|
}
|
|
16219
18516
|
}
|
|
@@ -16300,7 +18597,7 @@ var rnb = {
|
|
|
16300
18597
|
{ chords: ["vi9", "ii9", "Imaj7", "IVmaj7"], weight: 1, barsPerChord: 1 }
|
|
16301
18598
|
],
|
|
16302
18599
|
extensions: ["maj7", "maj9", "9", "11", "13", "7sus4"],
|
|
16303
|
-
voicing: "Rootless 4-note keys
|
|
18600
|
+
voicing: "Rootless 4-note keys (3-5-7-9 or 7-9-3-5) around middle C, pads above; chords often land a 16th early.",
|
|
16304
18601
|
harmonicRhythm: "1 chord per bar, or a 2-chord vamp of 2 bars each."
|
|
16305
18602
|
},
|
|
16306
18603
|
melody: {
|
|
@@ -16369,11 +18666,23 @@ var rnb = {
|
|
|
16369
18666
|
p90: [-5.5, -1, -4, -6.5, -8, -9.5, -10.5, -12, -13, -18.5],
|
|
16370
18667
|
source: TONAL_BALANCE_SOURCE
|
|
16371
18668
|
},
|
|
16372
|
-
sidechain:
|
|
18669
|
+
sidechain: {
|
|
18670
|
+
ducks: [
|
|
18671
|
+
{
|
|
18672
|
+
trigger: "kick",
|
|
18673
|
+
targets: ["bass"],
|
|
18674
|
+
ratio: 3,
|
|
18675
|
+
attackMs: 1,
|
|
18676
|
+
releaseMs: 80,
|
|
18677
|
+
depthDb: 3
|
|
18678
|
+
}
|
|
18679
|
+
],
|
|
18680
|
+
note: "Where kick and sub sustain together; or let the 808 be the kick."
|
|
18681
|
+
},
|
|
16373
18682
|
reverb: "Plate on the lead vocal, long hall on ad-libs and stacks; drums mostly dry.",
|
|
16374
18683
|
delay: "1/4 and dotted-1/8 vocal throws, filtered; ping-pong on ad-libs.",
|
|
16375
18684
|
stereo: "Lead vocal centred and intimate, harmony stack and keys wide.",
|
|
16376
|
-
lowEnd: "The sub (30-60 Hz) carries the weight; Roar or Pedal adds harmonics so it reads on phones. Chain: vocal EQ Eight > Compressor > Reverb/Echo; keys Chorus; master Glue
|
|
18685
|
+
lowEnd: "The sub (30-60 Hz) carries the weight; Roar or Pedal adds harmonics so it reads on phones. Chain: vocal EQ Eight > Compressor > Reverb/Echo; keys Chorus; gentle master Glue."
|
|
16377
18686
|
},
|
|
16378
18687
|
arrangement: {
|
|
16379
18688
|
variationEveryBars: 4,
|
|
@@ -16406,7 +18715,7 @@ var rnb = {
|
|
|
16406
18715
|
]
|
|
16407
18716
|
},
|
|
16408
18717
|
references: ["SZA", "H.E.R.", "Daniel Caesar", "David Tao", "Khalil Fong"],
|
|
16409
|
-
why: "R&B
|
|
18718
|
+
why: "R&B is intimate because it leaves space: a laid-back beat, a deep sub, 7th/9th chords that never fully resolve. The voice is the star (runs, stacks, ad-libs); the beat stays simple."
|
|
16410
18719
|
};
|
|
16411
18720
|
|
|
16412
18721
|
// ../../src/components/editor-ai/style/cards/synth-pop.ts
|
|
@@ -16674,7 +18983,19 @@ var synthPop = {
|
|
|
16674
18983
|
p90: [-12.3, -3.3, -3.3, -5.3, -6.3, -6.3, -7.3, -9.3, -12.3, -18.3],
|
|
16675
18984
|
source: TONAL_BALANCE_SOURCE
|
|
16676
18985
|
},
|
|
16677
|
-
sidechain:
|
|
18986
|
+
sidechain: {
|
|
18987
|
+
ducks: [
|
|
18988
|
+
{
|
|
18989
|
+
trigger: "kick",
|
|
18990
|
+
targets: ["pad"],
|
|
18991
|
+
ratio: 2,
|
|
18992
|
+
attackMs: 2,
|
|
18993
|
+
releaseMs: 150,
|
|
18994
|
+
depthDb: 2
|
|
18995
|
+
}
|
|
18996
|
+
],
|
|
18997
|
+
note: "Barely there: phrasing, not pumping, drives the song."
|
|
18998
|
+
},
|
|
16678
18999
|
reverb: "Reverb plate on snare/vocal; Gate the snare return for a short retro bloom.",
|
|
16679
19000
|
delay: "Echo eighths or slap on the hook, ducked below lead words.",
|
|
16680
19001
|
stereo: "Chorus-Ensemble on pads; center the lead vocal, bass and main hook.",
|
|
@@ -16960,7 +19281,19 @@ var synthwave = {
|
|
|
16960
19281
|
p90: [-10.2, -2.2, -2.2, -5.2, -7.2, -8.2, -9.2, -11.2, -14.2, -20.2],
|
|
16961
19282
|
source: TONAL_BALANCE_SOURCE
|
|
16962
19283
|
},
|
|
16963
|
-
sidechain:
|
|
19284
|
+
sidechain: {
|
|
19285
|
+
ducks: [
|
|
19286
|
+
{
|
|
19287
|
+
trigger: "kick",
|
|
19288
|
+
targets: ["bass", "pad"],
|
|
19289
|
+
ratio: 3,
|
|
19290
|
+
attackMs: 2,
|
|
19291
|
+
releaseMs: 160,
|
|
19292
|
+
depthDb: 3
|
|
19293
|
+
}
|
|
19294
|
+
],
|
|
19295
|
+
note: "A breath, not an EDM vacuum."
|
|
19296
|
+
},
|
|
16964
19297
|
reverb: "Reverb into Gate on the snare return; broad hall on pads.",
|
|
16965
19298
|
delay: "Echo dotted eighth on arp gaps, lowpassed feedback.",
|
|
16966
19299
|
stereo: "Chorus-Ensemble widens pads; lead and snare stay focused, sub mono.",
|
|
@@ -17247,7 +19580,19 @@ var techno = {
|
|
|
17247
19580
|
p90: [-3.5, 0, -3.5, -7.5, -10.5, -11.5, -12.5, -13.5, -14.5, -20.5],
|
|
17248
19581
|
source: TONAL_BALANCE_SOURCE
|
|
17249
19582
|
},
|
|
17250
|
-
sidechain:
|
|
19583
|
+
sidechain: {
|
|
19584
|
+
ducks: [
|
|
19585
|
+
{
|
|
19586
|
+
trigger: "kick",
|
|
19587
|
+
targets: ["bass"],
|
|
19588
|
+
ratio: 6,
|
|
19589
|
+
attackMs: 0.5,
|
|
19590
|
+
releaseMs: 150,
|
|
19591
|
+
depthDb: 8
|
|
19592
|
+
}
|
|
19593
|
+
],
|
|
19594
|
+
note: "The rumble ducks hard, back before the next kick."
|
|
19595
|
+
},
|
|
17251
19596
|
reverb: "Separate Reverb rumble return, then EQ Eight to remove highs and subsonic build-up; dry kick remains clear.",
|
|
17252
19597
|
delay: "Filtered dotted-eighth Echo on stabs; automate feedback at boundaries.",
|
|
17253
19598
|
stereo: "Kick and rumble mono; higher percussion and dub reflections can spread.",
|
|
@@ -17507,14 +19852,26 @@ var trance = {
|
|
|
17507
19852
|
},
|
|
17508
19853
|
starterClips: ["909 Core Kit Off Hat 125 bpm", "Techno Heavy 133 bpm"],
|
|
17509
19854
|
mix: {
|
|
17510
|
-
lufs: { min: -
|
|
19855
|
+
lufs: { min: -10, max: -8 },
|
|
17511
19856
|
tonalBalance: {
|
|
17512
19857
|
p10: [-18, -9, -9, -13, -15, -15, -15, -16, -19, -26],
|
|
17513
19858
|
p50: [-13, -5, -6, -10, -12, -12, -12, -13, -15, -21],
|
|
17514
19859
|
p90: [-8, -1, -3, -7, -9, -9, -9, -10, -11, -16],
|
|
17515
19860
|
source: TONAL_BALANCE_SOURCE
|
|
17516
19861
|
},
|
|
17517
|
-
sidechain:
|
|
19862
|
+
sidechain: {
|
|
19863
|
+
ducks: [
|
|
19864
|
+
{
|
|
19865
|
+
trigger: "kick",
|
|
19866
|
+
targets: ["bass", "pad"],
|
|
19867
|
+
ratio: 4,
|
|
19868
|
+
attackMs: 1,
|
|
19869
|
+
releaseMs: 140,
|
|
19870
|
+
depthDb: 5
|
|
19871
|
+
}
|
|
19872
|
+
],
|
|
19873
|
+
note: "Bass and pad recover into the offbeat."
|
|
19874
|
+
},
|
|
17518
19875
|
reverb: "Long Reverb hall in breakdowns; highpass the return and reduce it at the drop.",
|
|
17519
19876
|
delay: "Echo dotted eighth on the lead, filtered so the dry hook stays legible.",
|
|
17520
19877
|
stereo: "Wide detuned lead and pad above the lows; kick/sub mono. Check the hook in mono.",
|
|
@@ -17820,14 +20177,26 @@ var trap = {
|
|
|
17820
20177
|
"Sinister Run G Min 142 bpm"
|
|
17821
20178
|
],
|
|
17822
20179
|
mix: {
|
|
17823
|
-
lufs: { min: -
|
|
20180
|
+
lufs: { min: -10, max: -8 },
|
|
17824
20181
|
tonalBalance: {
|
|
17825
20182
|
p10: [-12.5, -8, -10, -13, -15.5, -17, -18, -19, -21.5, -28.5],
|
|
17826
20183
|
p50: [-7.5, -4, -7, -10, -12.5, -14, -15, -16, -17.5, -23.5],
|
|
17827
20184
|
p90: [-2.5, 0, -4, -7, -9.5, -11, -12, -13, -13.5, -18.5],
|
|
17828
20185
|
source: TONAL_BALANCE_SOURCE
|
|
17829
20186
|
},
|
|
17830
|
-
sidechain:
|
|
20187
|
+
sidechain: {
|
|
20188
|
+
ducks: [
|
|
20189
|
+
{
|
|
20190
|
+
trigger: "kick",
|
|
20191
|
+
targets: ["bass"],
|
|
20192
|
+
ratio: 4,
|
|
20193
|
+
attackMs: 0.5,
|
|
20194
|
+
releaseMs: 50,
|
|
20195
|
+
depthDb: 4
|
|
20196
|
+
}
|
|
20197
|
+
],
|
|
20198
|
+
note: "Fast and short, or arrange 808 and kick never to clash."
|
|
20199
|
+
},
|
|
17831
20200
|
reverb: "Big dark reverb on the melody; vocal upfront and nearly dry (short room, de-essed); drums dry.",
|
|
17832
20201
|
delay: "1/4 or 1/8 throws on vocal ends and melody.",
|
|
17833
20202
|
stereo: "Melody/pads wide, 808/kick/clap mono, hats slightly off-center.",
|
|
@@ -18114,7 +20483,19 @@ var ukGarage = {
|
|
|
18114
20483
|
p90: [-5.7, -0.7, -2.7, -6.7, -9.7, -10.7, -10.7, -11.7, -12.7, -17.7],
|
|
18115
20484
|
source: TONAL_BALANCE_SOURCE
|
|
18116
20485
|
},
|
|
18117
|
-
sidechain:
|
|
20486
|
+
sidechain: {
|
|
20487
|
+
ducks: [
|
|
20488
|
+
{
|
|
20489
|
+
trigger: "kick",
|
|
20490
|
+
targets: ["bass"],
|
|
20491
|
+
ratio: 3,
|
|
20492
|
+
attackMs: 1,
|
|
20493
|
+
releaseMs: 60,
|
|
20494
|
+
depthDb: 2
|
|
20495
|
+
}
|
|
20496
|
+
],
|
|
20497
|
+
note: "Clears the kick; keep the long gaps full."
|
|
20498
|
+
},
|
|
18118
20499
|
reverb: "Small Reverb room on clap/rim; a darker longer return for isolated vocal chops.",
|
|
18119
20500
|
delay: "Filtered Echo dotted eighth on chops; silence the feedback before busy hook entries.",
|
|
18120
20501
|
stereo: "Vocal chops and hats can answer left/right; kick, snare and sub stay centered.",
|
|
@@ -18237,14 +20618,16 @@ async function dispatch(command, io) {
|
|
|
18237
20618
|
const text = HELP[command.topic ?? "main"] ?? HELP.main;
|
|
18238
20619
|
io.stdout.write(
|
|
18239
20620
|
`${text}
|
|
18240
|
-
${command.topic &&
|
|
20621
|
+
${command.topic && !["setup", "skills", "login", "logout", "whoami"].includes(
|
|
20622
|
+
command.topic
|
|
20623
|
+
) ? `
|
|
18241
20624
|
${HELP.bridge}
|
|
18242
20625
|
` : ""}`
|
|
18243
20626
|
);
|
|
18244
20627
|
return EXIT_OK;
|
|
18245
20628
|
}
|
|
18246
20629
|
case "version":
|
|
18247
|
-
io.stdout.write(`${
|
|
20630
|
+
io.stdout.write(`${VERSION2}
|
|
18248
20631
|
`);
|
|
18249
20632
|
return EXIT_OK;
|
|
18250
20633
|
case "check":
|
|
@@ -18257,6 +20640,15 @@ ${HELP.bridge}
|
|
|
18257
20640
|
return mcp(command.bridge, io);
|
|
18258
20641
|
case "doctor":
|
|
18259
20642
|
return doctor(command, io);
|
|
20643
|
+
case "login":
|
|
20644
|
+
return login(command, io);
|
|
20645
|
+
case "logout":
|
|
20646
|
+
return logout(command.account, io);
|
|
20647
|
+
case "whoami":
|
|
20648
|
+
return whoami(command, io);
|
|
20649
|
+
case "login-poll":
|
|
20650
|
+
await runLoginPoller(command.file, createLogger("login", false));
|
|
20651
|
+
return EXIT_OK;
|
|
18260
20652
|
case "setup":
|
|
18261
20653
|
return setup(command, io);
|
|
18262
20654
|
case "skills-install": {
|
|
@@ -18319,33 +20711,62 @@ async function runJob(job, flags, mode, io, timeoutS) {
|
|
|
18319
20711
|
const onSigint = () => abort.abort();
|
|
18320
20712
|
process.once("SIGINT", onSigint);
|
|
18321
20713
|
const onProgress = progressPrinter(io, flags.verbose);
|
|
20714
|
+
const auth = await jobAuth(accountOrigin(flags.origin), "cli", log);
|
|
20715
|
+
if (auth.ended)
|
|
20716
|
+
io.stderr.write(`tutticode: ${auth.ended}
|
|
20717
|
+
`);
|
|
18322
20718
|
try {
|
|
20719
|
+
let outcome;
|
|
18323
20720
|
if (flags.noDaemon || io.openPage || !io.entry) {
|
|
18324
20721
|
const session = newSession(flags, mode, io, log);
|
|
18325
20722
|
try {
|
|
18326
|
-
|
|
20723
|
+
outcome = await session.run(job, {
|
|
18327
20724
|
timeoutMs: timeoutS * 1e3,
|
|
18328
20725
|
signal: abort.signal,
|
|
18329
|
-
onProgress
|
|
20726
|
+
onProgress,
|
|
20727
|
+
auth: auth.auth
|
|
18330
20728
|
});
|
|
18331
20729
|
} finally {
|
|
18332
20730
|
await session.close();
|
|
18333
20731
|
}
|
|
18334
|
-
}
|
|
18335
|
-
|
|
18336
|
-
|
|
18337
|
-
|
|
18338
|
-
|
|
18339
|
-
|
|
18340
|
-
|
|
18341
|
-
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
20732
|
+
} else
|
|
20733
|
+
outcome = await runViaDaemon(
|
|
20734
|
+
{ config: daemonConfig(flags, mode), entry: io.entry, log },
|
|
20735
|
+
job,
|
|
20736
|
+
timeoutS * 1e3,
|
|
20737
|
+
{
|
|
20738
|
+
signal: abort.signal,
|
|
20739
|
+
onProgress,
|
|
20740
|
+
onStatus: statusWriter(flags, mode, io)
|
|
20741
|
+
},
|
|
20742
|
+
auth.auth
|
|
20743
|
+
);
|
|
20744
|
+
if (outcome.auth?.rejected)
|
|
20745
|
+
io.stderr.write(`tutticode: ${rejectedLine(outcome.auth)}
|
|
20746
|
+
`);
|
|
20747
|
+
return outcome;
|
|
18345
20748
|
} finally {
|
|
18346
20749
|
process.removeListener("SIGINT", onSigint);
|
|
18347
20750
|
}
|
|
18348
20751
|
}
|
|
20752
|
+
function accountOrigin(origin) {
|
|
20753
|
+
return normalizeOrigin(origin ?? "") ?? BRIDGE_ORIGINS[0];
|
|
20754
|
+
}
|
|
20755
|
+
async function jobAuth(origin, client, log) {
|
|
20756
|
+
try {
|
|
20757
|
+
const token = await resolveToken(origin);
|
|
20758
|
+
return {
|
|
20759
|
+
auth: bridgeAuth(token, client),
|
|
20760
|
+
...token.ended ? { ended: token.ended } : {}
|
|
20761
|
+
};
|
|
20762
|
+
} catch (e) {
|
|
20763
|
+
log("warn", `sign-in unavailable: ${e.message}`);
|
|
20764
|
+
return { auth: bridgeAuth({ kind: "anonymous" }, client) };
|
|
20765
|
+
}
|
|
20766
|
+
}
|
|
20767
|
+
function rejectedLine(auth) {
|
|
20768
|
+
return `the page did not accept your sign-in (${auth.rejected}); this ran without sign-in. Run \`tutticode login\`.`;
|
|
20769
|
+
}
|
|
18349
20770
|
function progressPrinter(io, verbose) {
|
|
18350
20771
|
let last = "";
|
|
18351
20772
|
return (p) => {
|
|
@@ -18358,27 +20779,32 @@ function progressPrinter(io, verbose) {
|
|
|
18358
20779
|
last = line;
|
|
18359
20780
|
};
|
|
18360
20781
|
}
|
|
18361
|
-
async function readProgram(
|
|
18362
|
-
if (
|
|
20782
|
+
async function readProgram(file2, io) {
|
|
20783
|
+
if (file2 === "-") {
|
|
18363
20784
|
const chunks = [];
|
|
18364
20785
|
for await (const chunk of io.stdin)
|
|
18365
20786
|
chunks.push(Buffer.from(chunk));
|
|
18366
20787
|
return Buffer.concat(chunks).toString("utf8");
|
|
18367
20788
|
}
|
|
18368
20789
|
try {
|
|
18369
|
-
return (0,
|
|
20790
|
+
return (0, import_node_fs11.readFileSync)(file2, "utf8");
|
|
18370
20791
|
} catch (e) {
|
|
18371
20792
|
throw new UsageError(
|
|
18372
|
-
`Cannot read ${
|
|
20793
|
+
`Cannot read ${file2}: ${e.code ?? e.message}`
|
|
18373
20794
|
);
|
|
18374
20795
|
}
|
|
18375
20796
|
}
|
|
18376
|
-
function printError(outcome, json,
|
|
20797
|
+
function printError(outcome, json, file2, io) {
|
|
18377
20798
|
if (json)
|
|
18378
20799
|
io.stdout.write(
|
|
18379
20800
|
`${JSON.stringify(
|
|
18380
20801
|
{
|
|
18381
|
-
error: {
|
|
20802
|
+
error: {
|
|
20803
|
+
code: outcome.code,
|
|
20804
|
+
message: outcome.message,
|
|
20805
|
+
...outcome.limit ? { limit: outcome.limit } : {}
|
|
20806
|
+
},
|
|
20807
|
+
...outcome.code === "limit" ? { loginHint: { command: TUTTICODE_LOGIN_HINT_COMMAND } } : {},
|
|
18382
20808
|
...outcome.report ? { report: outcome.report } : {}
|
|
18383
20809
|
},
|
|
18384
20810
|
null,
|
|
@@ -18387,7 +20813,7 @@ function printError(outcome, json, file, io) {
|
|
|
18387
20813
|
`
|
|
18388
20814
|
);
|
|
18389
20815
|
if (outcome.report)
|
|
18390
|
-
for (const line of formatDiagnostics(outcome.report,
|
|
20816
|
+
for (const line of formatDiagnostics(outcome.report, file2))
|
|
18391
20817
|
io.stderr.write(`${line}
|
|
18392
20818
|
`);
|
|
18393
20819
|
io.stderr.write(`tutticode: ${outcome.code}: ${outcome.message}
|
|
@@ -18399,7 +20825,7 @@ async function check(command, io) {
|
|
|
18399
20825
|
const outcome = await runJob(
|
|
18400
20826
|
{
|
|
18401
20827
|
type: "check",
|
|
18402
|
-
id: (0,
|
|
20828
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
18403
20829
|
code,
|
|
18404
20830
|
...command.bpm !== void 0 ? { bpm: command.bpm } : {}
|
|
18405
20831
|
},
|
|
@@ -18430,8 +20856,15 @@ async function check(command, io) {
|
|
|
18430
20856
|
}
|
|
18431
20857
|
async function render(command, io) {
|
|
18432
20858
|
const code = await readProgram(command.file, io);
|
|
20859
|
+
const brandTagOff = command.options.brandTag === false || process.env.TUTTICODE_BRAND_TAG === "0";
|
|
18433
20860
|
const outcome = await runJob(
|
|
18434
|
-
{
|
|
20861
|
+
{
|
|
20862
|
+
type: "render",
|
|
20863
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
20864
|
+
code,
|
|
20865
|
+
...command.options,
|
|
20866
|
+
...brandTagOff ? { brandTag: false } : {}
|
|
20867
|
+
},
|
|
18435
20868
|
command.bridge,
|
|
18436
20869
|
modeFor(command.bridge, io),
|
|
18437
20870
|
io,
|
|
@@ -18462,10 +20895,45 @@ async function render(command, io) {
|
|
|
18462
20895
|
io.stdout.write(`${line}
|
|
18463
20896
|
`);
|
|
18464
20897
|
}
|
|
20898
|
+
if (outcome.meta.watermark)
|
|
20899
|
+
io.stderr.write(`${WATERMARK_LINE}
|
|
20900
|
+
`);
|
|
20901
|
+
const anonymous = outcome.auth ? !outcome.auth.signedIn : false;
|
|
20902
|
+
if (anonymous && brandTagOff && firstTime("brand-tag-anonymous", "install"))
|
|
20903
|
+
io.stderr.write(
|
|
20904
|
+
'The "Made with TuttiCode" file tag can be turned off after `tutticode login`.\n'
|
|
20905
|
+
);
|
|
20906
|
+
if (anonymous && io.interactive) {
|
|
20907
|
+
if (firstTime("terms", "install"))
|
|
20908
|
+
io.stderr.write(
|
|
20909
|
+
`TuttiCode renders are subject to ${tutticodeTermsUrl(
|
|
20910
|
+
accountOrigin(command.bridge.origin)
|
|
20911
|
+
)}
|
|
20912
|
+
`
|
|
20913
|
+
);
|
|
20914
|
+
if (firstTime("sign-in-hint", "day"))
|
|
20915
|
+
io.stderr.write(
|
|
20916
|
+
"Sign in free for longer renders without the voice tag: tutticode login\n"
|
|
20917
|
+
);
|
|
20918
|
+
}
|
|
18465
20919
|
return EXIT_OK;
|
|
18466
20920
|
}
|
|
18467
20921
|
async function open(command, io) {
|
|
18468
20922
|
const code = await readProgram(command.file, io);
|
|
20923
|
+
const flags = command.bridge;
|
|
20924
|
+
const url = codeLink(
|
|
20925
|
+
normalizeOrigin(flags.origin ?? "") ?? BRIDGE_ORIGINS[0],
|
|
20926
|
+
flags.locale ?? "en",
|
|
20927
|
+
code
|
|
20928
|
+
);
|
|
20929
|
+
if (flags.show === false) {
|
|
20930
|
+
io.stdout.write(
|
|
20931
|
+
`No browser window was opened (--headless). Open the program in TuttiCode:
|
|
20932
|
+
${url}
|
|
20933
|
+
`
|
|
20934
|
+
);
|
|
20935
|
+
return EXIT_OK;
|
|
20936
|
+
}
|
|
18469
20937
|
const outcome = await runJob(
|
|
18470
20938
|
{ type: "open", code },
|
|
18471
20939
|
command.bridge,
|
|
@@ -18480,20 +20948,23 @@ async function open(command, io) {
|
|
|
18480
20948
|
command.file,
|
|
18481
20949
|
io
|
|
18482
20950
|
);
|
|
18483
|
-
io.stdout.write(
|
|
20951
|
+
io.stdout.write(`Opened in TuttiCode: ${url}
|
|
20952
|
+
`);
|
|
18484
20953
|
return EXIT_OK;
|
|
18485
20954
|
}
|
|
18486
20955
|
async function mcp(flags, io) {
|
|
18487
20956
|
const log = createLogger("mcp", flags.verbose);
|
|
20957
|
+
const origin = accountOrigin(flags.origin);
|
|
18488
20958
|
const { backend, close } = sessionBackend(
|
|
18489
20959
|
(mode) => newSession(
|
|
18490
20960
|
flags,
|
|
18491
20961
|
flags.show ? "show" : mode,
|
|
18492
20962
|
{ ...io, stderr: process.stderr },
|
|
18493
20963
|
log
|
|
18494
|
-
)
|
|
20964
|
+
),
|
|
20965
|
+
() => jobAuth(origin, "mcp", log)
|
|
18495
20966
|
);
|
|
18496
|
-
log("info", `mcp server ${
|
|
20967
|
+
log("info", `mcp server ${VERSION2} starting`);
|
|
18497
20968
|
await serveMcpStdio({
|
|
18498
20969
|
input: io.stdin,
|
|
18499
20970
|
output: io.stdout,
|
|
@@ -18506,7 +20977,8 @@ async function mcp(flags, io) {
|
|
|
18506
20977
|
code
|
|
18507
20978
|
),
|
|
18508
20979
|
// --headless: no window, not even for code_open
|
|
18509
|
-
canShow: flags.show !== false
|
|
20980
|
+
canShow: flags.show !== false,
|
|
20981
|
+
account: () => accountState(origin)
|
|
18510
20982
|
});
|
|
18511
20983
|
await close();
|
|
18512
20984
|
return EXIT_OK;
|
|
@@ -18590,14 +21062,313 @@ async function doctor(command, io) {
|
|
|
18590
21062
|
)}) in ${info.browser.version ?? "the browser"}` : `${info.browser.version ?? "the browser"} accepted no Local Network Access grant (it may not enforce it)`
|
|
18591
21063
|
);
|
|
18592
21064
|
}
|
|
21065
|
+
await accountChecks(accountOrigin(flags.origin), command.fix, add);
|
|
18593
21066
|
const ok = results.every((r2) => r2.ok || r2.check === "local network access");
|
|
18594
21067
|
if (command.json)
|
|
18595
21068
|
io.stdout.write(
|
|
18596
|
-
`${JSON.stringify({ ok, version:
|
|
21069
|
+
`${JSON.stringify({ ok, version: VERSION2, checks: results }, null, 2)}
|
|
18597
21070
|
`
|
|
18598
21071
|
);
|
|
18599
21072
|
return ok ? EXIT_OK : EXIT_BRIDGE;
|
|
18600
21073
|
}
|
|
21074
|
+
async function accountChecks(origin, fix, add) {
|
|
21075
|
+
const state = await accountState(origin);
|
|
21076
|
+
switch (state.state) {
|
|
21077
|
+
case "signed-in":
|
|
21078
|
+
add(
|
|
21079
|
+
"account",
|
|
21080
|
+
true,
|
|
21081
|
+
`signed in as ${signedInAs(state.me)}${state.tokenKind === "pat" ? " with TUTTICODE_TOKEN" : ""}`
|
|
21082
|
+
);
|
|
21083
|
+
break;
|
|
21084
|
+
case "anonymous":
|
|
21085
|
+
add("account", true, `not signed in: ${anonymousLimitsText()}`);
|
|
21086
|
+
break;
|
|
21087
|
+
case "rejected":
|
|
21088
|
+
add(
|
|
21089
|
+
"account",
|
|
21090
|
+
false,
|
|
21091
|
+
`sign-in rejected (${state.message}): run tutticode login`
|
|
21092
|
+
);
|
|
21093
|
+
break;
|
|
21094
|
+
case "unreachable":
|
|
21095
|
+
add("account", false, `could not check the sign-in: ${state.message}`);
|
|
21096
|
+
}
|
|
21097
|
+
if (envToken())
|
|
21098
|
+
add(
|
|
21099
|
+
"token override",
|
|
21100
|
+
true,
|
|
21101
|
+
"TUTTICODE_TOKEN is set: it wins over the stored sign-in"
|
|
21102
|
+
);
|
|
21103
|
+
const path = credentialsPath();
|
|
21104
|
+
if (!(0, import_node_fs11.existsSync)(path))
|
|
21105
|
+
return;
|
|
21106
|
+
let access = checkFileAccess(path);
|
|
21107
|
+
if (fix && access && !access.ok) {
|
|
21108
|
+
try {
|
|
21109
|
+
fixFileAccess(path);
|
|
21110
|
+
} catch (e) {
|
|
21111
|
+
if (!(e instanceof PrivateDirError))
|
|
21112
|
+
throw e;
|
|
21113
|
+
add("credentials folder", false, e.message);
|
|
21114
|
+
}
|
|
21115
|
+
access = checkFileAccess(path);
|
|
21116
|
+
}
|
|
21117
|
+
if (!access)
|
|
21118
|
+
add("credentials file", true, `${path} (permissions not checked)`);
|
|
21119
|
+
else
|
|
21120
|
+
add(
|
|
21121
|
+
"credentials file",
|
|
21122
|
+
access.ok,
|
|
21123
|
+
access.ok ? `${path}: ${access.detail}` : `${path} is too open (${access.detail}): run tutticode doctor --fix`
|
|
21124
|
+
);
|
|
21125
|
+
}
|
|
21126
|
+
function anonymousLimitsText() {
|
|
21127
|
+
const limits = accountLimits({ state: "anonymous" });
|
|
21128
|
+
return `renders up to ${limits.maxSeconds} s, a voice tag over ${limits.watermarkAfterSeconds} s (sign in free: tutticode login)`;
|
|
21129
|
+
}
|
|
21130
|
+
async function login(command, io) {
|
|
21131
|
+
const origin = accountOrigin(command.account.origin);
|
|
21132
|
+
const locale = command.account.locale ?? "en";
|
|
21133
|
+
ensurePrivateDir();
|
|
21134
|
+
if (!command.noWait && !io.interactive) {
|
|
21135
|
+
io.stderr.write(
|
|
21136
|
+
`tutticode: login needs a person: run it in a terminal, or use ${TUTTICODE_LOGIN_HINT_COMMAND}
|
|
21137
|
+
`
|
|
21138
|
+
);
|
|
21139
|
+
return EXIT_USAGE;
|
|
21140
|
+
}
|
|
21141
|
+
if (envToken())
|
|
21142
|
+
io.stderr.write(
|
|
21143
|
+
"Note: TUTTICODE_TOKEN is set; it wins over this sign-in until you unset it.\n"
|
|
21144
|
+
);
|
|
21145
|
+
const name = deviceName();
|
|
21146
|
+
const abort = new AbortController();
|
|
21147
|
+
const onSigint = () => abort.abort();
|
|
21148
|
+
process.once("SIGINT", onSigint);
|
|
21149
|
+
try {
|
|
21150
|
+
let tokens;
|
|
21151
|
+
if (!command.device) {
|
|
21152
|
+
const remote = !io.openBrowser && (!!process.env.SSH_CONNECTION || process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY);
|
|
21153
|
+
tokens = await loopbackLogin({
|
|
21154
|
+
origin,
|
|
21155
|
+
locale,
|
|
21156
|
+
deviceName: name,
|
|
21157
|
+
signal: abort.signal,
|
|
21158
|
+
openBrowser: io.openBrowser ?? (remote ? () => void 0 : openExternal),
|
|
21159
|
+
onUrl: (url) => io.stderr.write(
|
|
21160
|
+
remote ? `No browser can be opened here. Open this link in a browser on this computer:
|
|
21161
|
+
${url}
|
|
21162
|
+
Or sign in with a code on any device: tutticode login --device
|
|
21163
|
+
` : `Opening your browser to sign in. If it does not open, go to:
|
|
21164
|
+
${url}
|
|
21165
|
+
`
|
|
21166
|
+
)
|
|
21167
|
+
});
|
|
21168
|
+
} else {
|
|
21169
|
+
const device = await startDeviceLogin(origin, name);
|
|
21170
|
+
const complete = device.verificationUriComplete ?? device.verificationUri;
|
|
21171
|
+
if (command.noWait) {
|
|
21172
|
+
const file2 = writePendingLogin({
|
|
21173
|
+
origin,
|
|
21174
|
+
deviceCode: device.deviceCode,
|
|
21175
|
+
userCode: device.userCode,
|
|
21176
|
+
verificationUri: device.verificationUri,
|
|
21177
|
+
...device.verificationUriComplete ? { verificationUriComplete: device.verificationUriComplete } : {},
|
|
21178
|
+
expiresAt: new Date(
|
|
21179
|
+
Date.now() + device.expiresIn * 1e3
|
|
21180
|
+
).toISOString(),
|
|
21181
|
+
interval: device.interval,
|
|
21182
|
+
deviceName: name
|
|
21183
|
+
});
|
|
21184
|
+
if (io.spawnLoginPoller)
|
|
21185
|
+
io.spawnLoginPoller(file2);
|
|
21186
|
+
else if (io.entry)
|
|
21187
|
+
spawnLoginPoller(io.entry, file2);
|
|
21188
|
+
else
|
|
21189
|
+
throw new Error("Cannot start the background sign-in.");
|
|
21190
|
+
if (command.json)
|
|
21191
|
+
io.stdout.write(
|
|
21192
|
+
`${JSON.stringify(
|
|
21193
|
+
{
|
|
21194
|
+
verificationUri: device.verificationUri,
|
|
21195
|
+
verificationUriComplete: complete,
|
|
21196
|
+
userCode: device.userCode,
|
|
21197
|
+
expiresIn: device.expiresIn
|
|
21198
|
+
},
|
|
21199
|
+
null,
|
|
21200
|
+
2
|
|
21201
|
+
)}
|
|
21202
|
+
`
|
|
21203
|
+
);
|
|
21204
|
+
else
|
|
21205
|
+
io.stdout.write(
|
|
21206
|
+
`To sign in, open ${complete}
|
|
21207
|
+
(or open ${device.verificationUri} and enter the code ${device.userCode})
|
|
21208
|
+
within ${Math.round(
|
|
21209
|
+
device.expiresIn / 60
|
|
21210
|
+
)} minutes. tutticode finishes the sign-in in the background once it is approved; until then renders run without sign-in.
|
|
21211
|
+
`
|
|
21212
|
+
);
|
|
21213
|
+
return EXIT_OK;
|
|
21214
|
+
}
|
|
21215
|
+
io.stderr.write(
|
|
21216
|
+
`To sign in, open ${device.verificationUri} on any device and enter the code
|
|
21217
|
+
|
|
21218
|
+
${device.userCode}
|
|
21219
|
+
|
|
21220
|
+
or open ${complete}
|
|
21221
|
+
or scan it with a phone:
|
|
21222
|
+
|
|
21223
|
+
${renderUnicodeCompact(
|
|
21224
|
+
complete,
|
|
21225
|
+
{ border: 1 }
|
|
21226
|
+
)}
|
|
21227
|
+
|
|
21228
|
+
Waiting for approval (the code expires in ${Math.round(
|
|
21229
|
+
device.expiresIn / 60
|
|
21230
|
+
)} minutes)...
|
|
21231
|
+
`
|
|
21232
|
+
);
|
|
21233
|
+
tokens = await pollDeviceToken(origin, device, {
|
|
21234
|
+
...io.loginPoll,
|
|
21235
|
+
signal: abort.signal
|
|
21236
|
+
});
|
|
21237
|
+
}
|
|
21238
|
+
const state = await completeLogin(origin, tokens, name);
|
|
21239
|
+
io.stdout.write(
|
|
21240
|
+
state.state === "signed-in" ? `Signed in as ${signedInAs(state.me)}
|
|
21241
|
+
` : "Signed in.\n"
|
|
21242
|
+
);
|
|
21243
|
+
return EXIT_OK;
|
|
21244
|
+
} catch (e) {
|
|
21245
|
+
if (e instanceof LoginError || e instanceof ApiUnreachable) {
|
|
21246
|
+
io.stderr.write(`tutticode: ${e.message}
|
|
21247
|
+
`);
|
|
21248
|
+
return EXIT_BRIDGE;
|
|
21249
|
+
}
|
|
21250
|
+
throw e;
|
|
21251
|
+
} finally {
|
|
21252
|
+
process.removeListener("SIGINT", onSigint);
|
|
21253
|
+
}
|
|
21254
|
+
}
|
|
21255
|
+
async function logout(flags, io) {
|
|
21256
|
+
const origin = accountOrigin(flags.origin);
|
|
21257
|
+
const accountPage = `${origin}${ACCOUNT_PATH}`;
|
|
21258
|
+
(0, import_node_fs11.rmSync)(pendingLoginPath(origin), { force: true });
|
|
21259
|
+
if (envToken())
|
|
21260
|
+
io.stderr.write(
|
|
21261
|
+
`TUTTICODE_TOKEN is set: it keeps signing tutticode in until you unset it. Revoke it on the account page: ${accountPage}
|
|
21262
|
+
`
|
|
21263
|
+
);
|
|
21264
|
+
const stored = readAccount(origin);
|
|
21265
|
+
if (!stored) {
|
|
21266
|
+
io.stdout.write(`Not signed in on this computer (${origin}).
|
|
21267
|
+
`);
|
|
21268
|
+
return EXIT_OK;
|
|
21269
|
+
}
|
|
21270
|
+
const revoked = await revokeToken(origin, stored.refreshToken);
|
|
21271
|
+
await saveAccount(origin, void 0);
|
|
21272
|
+
if (!revoked)
|
|
21273
|
+
io.stderr.write(
|
|
21274
|
+
`Warning: ${origin} could not be reached to end the sign-in; it is deleted here. Revoke this device on the account page: ${accountPage}
|
|
21275
|
+
`
|
|
21276
|
+
);
|
|
21277
|
+
io.stdout.write(
|
|
21278
|
+
`Signed out${stored.email ? ` (${stored.email})` : ""}. Other devices and access tokens: ${accountPage}
|
|
21279
|
+
`
|
|
21280
|
+
);
|
|
21281
|
+
return EXIT_OK;
|
|
21282
|
+
}
|
|
21283
|
+
async function whoami(command, io) {
|
|
21284
|
+
const origin = accountOrigin(command.account.origin);
|
|
21285
|
+
const state = await accountState(origin);
|
|
21286
|
+
const pending = state.state === "signed-in" ? void 0 : pendingLogin(origin);
|
|
21287
|
+
if (command.json)
|
|
21288
|
+
io.stdout.write(`${JSON.stringify(whoamiJson(state, pending), null, 2)}
|
|
21289
|
+
`);
|
|
21290
|
+
else
|
|
21291
|
+
for (const line of whoamiLines(state, pending))
|
|
21292
|
+
io.stdout.write(`${line}
|
|
21293
|
+
`);
|
|
21294
|
+
if (state.state === "signed-in")
|
|
21295
|
+
return EXIT_OK;
|
|
21296
|
+
return state.state === "anonymous" ? EXIT_ANONYMOUS : EXIT_BRIDGE;
|
|
21297
|
+
}
|
|
21298
|
+
function whoamiJson(state, pending) {
|
|
21299
|
+
const pendingJson = pending ? {
|
|
21300
|
+
pendingLogin: {
|
|
21301
|
+
userCode: pending.userCode,
|
|
21302
|
+
verificationUri: pending.verificationUri,
|
|
21303
|
+
expiresAt: pending.expiresAt
|
|
21304
|
+
}
|
|
21305
|
+
} : {};
|
|
21306
|
+
switch (state.state) {
|
|
21307
|
+
case "signed-in": {
|
|
21308
|
+
const { me } = state;
|
|
21309
|
+
return {
|
|
21310
|
+
signedIn: true,
|
|
21311
|
+
tokenKind: state.tokenKind,
|
|
21312
|
+
user: me.user,
|
|
21313
|
+
plan: me.plan,
|
|
21314
|
+
limits: me.limits,
|
|
21315
|
+
usageToday: me.usageToday,
|
|
21316
|
+
expiresAt: me.expiresAt,
|
|
21317
|
+
deviceName: me.deviceName,
|
|
21318
|
+
...me.termsUpdate ? { termsUpdate: me.termsUpdate } : {}
|
|
21319
|
+
};
|
|
21320
|
+
}
|
|
21321
|
+
case "anonymous":
|
|
21322
|
+
return {
|
|
21323
|
+
signedIn: false,
|
|
21324
|
+
tokenKind: "anonymous",
|
|
21325
|
+
limits: accountLimits(state),
|
|
21326
|
+
loginHint: LOGIN_HINT,
|
|
21327
|
+
...pendingJson
|
|
21328
|
+
};
|
|
21329
|
+
default:
|
|
21330
|
+
return {
|
|
21331
|
+
signedIn: false,
|
|
21332
|
+
tokenKind: state.tokenKind,
|
|
21333
|
+
error: {
|
|
21334
|
+
code: state.state === "rejected" ? state.code ?? "rejected" : "unreachable",
|
|
21335
|
+
message: state.message
|
|
21336
|
+
},
|
|
21337
|
+
...pendingJson
|
|
21338
|
+
};
|
|
21339
|
+
}
|
|
21340
|
+
}
|
|
21341
|
+
function whoamiLines(state, pending) {
|
|
21342
|
+
const waiting = pending ? [
|
|
21343
|
+
`A sign-in is waiting for approval: open ${pending.verificationUriComplete ?? pending.verificationUri} (code ${pending.userCode}).`
|
|
21344
|
+
] : [];
|
|
21345
|
+
switch (state.state) {
|
|
21346
|
+
case "signed-in": {
|
|
21347
|
+
const { me } = state;
|
|
21348
|
+
const until = me.expiresAt ? `, until ${me.expiresAt.slice(0, 10)}` : "";
|
|
21349
|
+
const lines = [
|
|
21350
|
+
`Signed in as ${signedInAs(me)}${me.user.nickname ? ` - ${me.user.nickname}` : ""}`,
|
|
21351
|
+
state.tokenKind === "pat" ? `With TUTTICODE_TOKEN (an access token)${until}` : `Signed-in device: ${me.deviceName ?? "this computer"}${until}`,
|
|
21352
|
+
`Limits: renders up to ${me.limits.maxSeconds} s, no voice tag`,
|
|
21353
|
+
`Today: ${me.usageToday.renders} render${me.usageToday.renders === 1 ? "" : "s"}, ${Math.round(me.usageToday.seconds)} s of audio`
|
|
21354
|
+
];
|
|
21355
|
+
if (me.termsUpdate && firstTime(`terms-update:${me.termsUpdate}`, "install"))
|
|
21356
|
+
lines.push(
|
|
21357
|
+
`The TuttiCode terms changed: ${me.termsUpdate} (the next tutticode login accepts them)`
|
|
21358
|
+
);
|
|
21359
|
+
return lines;
|
|
21360
|
+
}
|
|
21361
|
+
case "anonymous":
|
|
21362
|
+
return [`Not signed in: ${anonymousLimitsText()}.`, ...waiting];
|
|
21363
|
+
case "rejected":
|
|
21364
|
+
return [
|
|
21365
|
+
state.message.includes("tutticode login") ? state.message : `Your sign-in was rejected (${state.message}). Run tutticode login.`,
|
|
21366
|
+
...waiting
|
|
21367
|
+
];
|
|
21368
|
+
case "unreachable":
|
|
21369
|
+
return [`Could not check the sign-in: ${state.message}`];
|
|
21370
|
+
}
|
|
21371
|
+
}
|
|
18601
21372
|
async function setup(command, io) {
|
|
18602
21373
|
const steps = policySteps(command.undo);
|
|
18603
21374
|
if (command.undo) {
|
|
@@ -18675,7 +21446,7 @@ function askOnTerminal(io) {
|
|
|
18675
21446
|
}
|
|
18676
21447
|
|
|
18677
21448
|
// src/bin.ts
|
|
18678
|
-
setSkillsDir((0,
|
|
21449
|
+
setSkillsDir((0, import_node_path12.join)(__dirname, "skills"));
|
|
18679
21450
|
main(process.argv.slice(2), {
|
|
18680
21451
|
stdout: process.stdout,
|
|
18681
21452
|
stderr: process.stderr,
|